##// END OF EJS Templates
templatekw: switch remainder of _showlist template keywords to new API
Yuya Nishihara -
r36616:c3f9d0c3 default
parent child Browse files
Show More
@@ -1,390 +1,391
1 # lfs - hash-preserving large file support using Git-LFS protocol
1 # lfs - hash-preserving large file support using Git-LFS protocol
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """lfs - large file support (EXPERIMENTAL)
8 """lfs - large file support (EXPERIMENTAL)
9
9
10 This extension allows large files to be tracked outside of the normal
10 This extension allows large files to be tracked outside of the normal
11 repository storage and stored on a centralized server, similar to the
11 repository storage and stored on a centralized server, similar to the
12 ``largefiles`` extension. The ``git-lfs`` protocol is used when
12 ``largefiles`` extension. The ``git-lfs`` protocol is used when
13 communicating with the server, so existing git infrastructure can be
13 communicating with the server, so existing git infrastructure can be
14 harnessed. Even though the files are stored outside of the repository,
14 harnessed. Even though the files are stored outside of the repository,
15 they are still integrity checked in the same manner as normal files.
15 they are still integrity checked in the same manner as normal files.
16
16
17 The files stored outside of the repository are downloaded on demand,
17 The files stored outside of the repository are downloaded on demand,
18 which reduces the time to clone, and possibly the local disk usage.
18 which reduces the time to clone, and possibly the local disk usage.
19 This changes fundamental workflows in a DVCS, so careful thought
19 This changes fundamental workflows in a DVCS, so careful thought
20 should be given before deploying it. :hg:`convert` can be used to
20 should be given before deploying it. :hg:`convert` can be used to
21 convert LFS repositories to normal repositories that no longer
21 convert LFS repositories to normal repositories that no longer
22 require this extension, and do so without changing the commit hashes.
22 require this extension, and do so without changing the commit hashes.
23 This allows the extension to be disabled if the centralized workflow
23 This allows the extension to be disabled if the centralized workflow
24 becomes burdensome. However, the pre and post convert clones will
24 becomes burdensome. However, the pre and post convert clones will
25 not be able to communicate with each other unless the extension is
25 not be able to communicate with each other unless the extension is
26 enabled on both.
26 enabled on both.
27
27
28 To start a new repository, or to add LFS files to an existing one, just
28 To start a new repository, or to add LFS files to an existing one, just
29 create an ``.hglfs`` file as described below in the root directory of
29 create an ``.hglfs`` file as described below in the root directory of
30 the repository. Typically, this file should be put under version
30 the repository. Typically, this file should be put under version
31 control, so that the settings will propagate to other repositories with
31 control, so that the settings will propagate to other repositories with
32 push and pull. During any commit, Mercurial will consult this file to
32 push and pull. During any commit, Mercurial will consult this file to
33 determine if an added or modified file should be stored externally. The
33 determine if an added or modified file should be stored externally. The
34 type of storage depends on the characteristics of the file at each
34 type of storage depends on the characteristics of the file at each
35 commit. A file that is near a size threshold may switch back and forth
35 commit. A file that is near a size threshold may switch back and forth
36 between LFS and normal storage, as needed.
36 between LFS and normal storage, as needed.
37
37
38 Alternately, both normal repositories and largefile controlled
38 Alternately, both normal repositories and largefile controlled
39 repositories can be converted to LFS by using :hg:`convert` and the
39 repositories can be converted to LFS by using :hg:`convert` and the
40 ``lfs.track`` config option described below. The ``.hglfs`` file
40 ``lfs.track`` config option described below. The ``.hglfs`` file
41 should then be created and added, to control subsequent LFS selection.
41 should then be created and added, to control subsequent LFS selection.
42 The hashes are also unchanged in this case. The LFS and non-LFS
42 The hashes are also unchanged in this case. The LFS and non-LFS
43 repositories can be distinguished because the LFS repository will
43 repositories can be distinguished because the LFS repository will
44 abort any command if this extension is disabled.
44 abort any command if this extension is disabled.
45
45
46 Committed LFS files are held locally, until the repository is pushed.
46 Committed LFS files are held locally, until the repository is pushed.
47 Prior to pushing the normal repository data, the LFS files that are
47 Prior to pushing the normal repository data, the LFS files that are
48 tracked by the outgoing commits are automatically uploaded to the
48 tracked by the outgoing commits are automatically uploaded to the
49 configured central server. No LFS files are transferred on
49 configured central server. No LFS files are transferred on
50 :hg:`pull` or :hg:`clone`. Instead, the files are downloaded on
50 :hg:`pull` or :hg:`clone`. Instead, the files are downloaded on
51 demand as they need to be read, if a cached copy cannot be found
51 demand as they need to be read, if a cached copy cannot be found
52 locally. Both committing and downloading an LFS file will link the
52 locally. Both committing and downloading an LFS file will link the
53 file to a usercache, to speed up future access. See the `usercache`
53 file to a usercache, to speed up future access. See the `usercache`
54 config setting described below.
54 config setting described below.
55
55
56 .hglfs::
56 .hglfs::
57
57
58 The extension reads its configuration from a versioned ``.hglfs``
58 The extension reads its configuration from a versioned ``.hglfs``
59 configuration file found in the root of the working directory. The
59 configuration file found in the root of the working directory. The
60 ``.hglfs`` file uses the same syntax as all other Mercurial
60 ``.hglfs`` file uses the same syntax as all other Mercurial
61 configuration files. It uses a single section, ``[track]``.
61 configuration files. It uses a single section, ``[track]``.
62
62
63 The ``[track]`` section specifies which files are stored as LFS (or
63 The ``[track]`` section specifies which files are stored as LFS (or
64 not). Each line is keyed by a file pattern, with a predicate value.
64 not). Each line is keyed by a file pattern, with a predicate value.
65 The first file pattern match is used, so put more specific patterns
65 The first file pattern match is used, so put more specific patterns
66 first. The available predicates are ``all()``, ``none()``, and
66 first. The available predicates are ``all()``, ``none()``, and
67 ``size()``. See "hg help filesets.size" for the latter.
67 ``size()``. See "hg help filesets.size" for the latter.
68
68
69 Example versioned ``.hglfs`` file::
69 Example versioned ``.hglfs`` file::
70
70
71 [track]
71 [track]
72 # No Makefile or python file, anywhere, will be LFS
72 # No Makefile or python file, anywhere, will be LFS
73 **Makefile = none()
73 **Makefile = none()
74 **.py = none()
74 **.py = none()
75
75
76 **.zip = all()
76 **.zip = all()
77 **.exe = size(">1MB")
77 **.exe = size(">1MB")
78
78
79 # Catchall for everything not matched above
79 # Catchall for everything not matched above
80 ** = size(">10MB")
80 ** = size(">10MB")
81
81
82 Configs::
82 Configs::
83
83
84 [lfs]
84 [lfs]
85 # Remote endpoint. Multiple protocols are supported:
85 # Remote endpoint. Multiple protocols are supported:
86 # - http(s)://user:pass@example.com/path
86 # - http(s)://user:pass@example.com/path
87 # git-lfs endpoint
87 # git-lfs endpoint
88 # - file:///tmp/path
88 # - file:///tmp/path
89 # local filesystem, usually for testing
89 # local filesystem, usually for testing
90 # if unset, lfs will prompt setting this when it must use this value.
90 # if unset, lfs will prompt setting this when it must use this value.
91 # (default: unset)
91 # (default: unset)
92 url = https://example.com/repo.git/info/lfs
92 url = https://example.com/repo.git/info/lfs
93
93
94 # Which files to track in LFS. Path tests are "**.extname" for file
94 # Which files to track in LFS. Path tests are "**.extname" for file
95 # extensions, and "path:under/some/directory" for path prefix. Both
95 # extensions, and "path:under/some/directory" for path prefix. Both
96 # are relative to the repository root.
96 # are relative to the repository root.
97 # File size can be tested with the "size()" fileset, and tests can be
97 # File size can be tested with the "size()" fileset, and tests can be
98 # joined with fileset operators. (See "hg help filesets.operators".)
98 # joined with fileset operators. (See "hg help filesets.operators".)
99 #
99 #
100 # Some examples:
100 # Some examples:
101 # - all() # everything
101 # - all() # everything
102 # - none() # nothing
102 # - none() # nothing
103 # - size(">20MB") # larger than 20MB
103 # - size(">20MB") # larger than 20MB
104 # - !**.txt # anything not a *.txt file
104 # - !**.txt # anything not a *.txt file
105 # - **.zip | **.tar.gz | **.7z # some types of compressed files
105 # - **.zip | **.tar.gz | **.7z # some types of compressed files
106 # - path:bin # files under "bin" in the project root
106 # - path:bin # files under "bin" in the project root
107 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
107 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
108 # | (path:bin & !path:/bin/README) | size(">1GB")
108 # | (path:bin & !path:/bin/README) | size(">1GB")
109 # (default: none())
109 # (default: none())
110 #
110 #
111 # This is ignored if there is a tracked '.hglfs' file, and this setting
111 # This is ignored if there is a tracked '.hglfs' file, and this setting
112 # will eventually be deprecated and removed.
112 # will eventually be deprecated and removed.
113 track = size(">10M")
113 track = size(">10M")
114
114
115 # how many times to retry before giving up on transferring an object
115 # how many times to retry before giving up on transferring an object
116 retry = 5
116 retry = 5
117
117
118 # the local directory to store lfs files for sharing across local clones.
118 # the local directory to store lfs files for sharing across local clones.
119 # If not set, the cache is located in an OS specific cache location.
119 # If not set, the cache is located in an OS specific cache location.
120 usercache = /path/to/global/cache
120 usercache = /path/to/global/cache
121 """
121 """
122
122
123 from __future__ import absolute_import
123 from __future__ import absolute_import
124
124
125 from mercurial.i18n import _
125 from mercurial.i18n import _
126
126
127 from mercurial import (
127 from mercurial import (
128 bundle2,
128 bundle2,
129 changegroup,
129 changegroup,
130 cmdutil,
130 cmdutil,
131 config,
131 config,
132 context,
132 context,
133 error,
133 error,
134 exchange,
134 exchange,
135 extensions,
135 extensions,
136 filelog,
136 filelog,
137 fileset,
137 fileset,
138 hg,
138 hg,
139 localrepo,
139 localrepo,
140 minifileset,
140 minifileset,
141 node,
141 node,
142 pycompat,
142 pycompat,
143 registrar,
143 registrar,
144 revlog,
144 revlog,
145 scmutil,
145 scmutil,
146 templatekw,
146 templatekw,
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')
359 @templatekeyword('lfs_files', requires={'ctx', 'templ'})
360 def lfsfiles(repo, ctx, **args):
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 args = pycompat.byteskwargs(args)
363 ctx = context.resource(mapping, 'ctx')
364 templ = context.resource(mapping, 'templ')
364
365
365 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
366 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
366 files = sorted(pointers.keys())
367 files = sorted(pointers.keys())
367
368
368 def pointer(v):
369 def pointer(v):
369 # In the file spec, version is first and the other keys are sorted.
370 # In the file spec, version is first and the other keys are sorted.
370 sortkeyfunc = lambda x: (x[0] != 'version', x)
371 sortkeyfunc = lambda x: (x[0] != 'version', x)
371 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
372 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
372 return util.sortdict(items)
373 return util.sortdict(items)
373
374
374 makemap = lambda v: {
375 makemap = lambda v: {
375 'file': v,
376 'file': v,
376 'lfsoid': pointers[v].oid() if pointers[v] else None,
377 'lfsoid': pointers[v].oid() if pointers[v] else None,
377 'lfspointer': templatekw.hybriddict(pointer(v)),
378 'lfspointer': templatekw.hybriddict(pointer(v)),
378 }
379 }
379
380
380 # TODO: make the separator ', '?
381 # TODO: make the separator ', '?
381 f = templatekw._showlist('lfs_file', files, args['templ'], args)
382 f = templatekw._showlist('lfs_file', files, templ, mapping)
382 return templatekw._hybrid(f, files, makemap, pycompat.identity)
383 return templatekw._hybrid(f, files, makemap, pycompat.identity)
383
384
384 @command('debuglfsupload',
385 @command('debuglfsupload',
385 [('r', 'rev', [], _('upload large files introduced by REV'))])
386 [('r', 'rev', [], _('upload large files introduced by REV'))])
386 def debuglfsupload(ui, repo, **opts):
387 def debuglfsupload(ui, repo, **opts):
387 """upload lfs blobs added by the working copy parent or given revisions"""
388 """upload lfs blobs added by the working copy parent or given revisions"""
388 revs = opts.get(r'rev', [])
389 revs = opts.get(r'rev', [])
389 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
390 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
390 wrapper.uploadblobs(repo, pointers)
391 wrapper.uploadblobs(repo, pointers)
@@ -1,985 +1,987
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 util,
26 util,
27 )
27 )
28
28
29 class _hybrid(object):
29 class _hybrid(object):
30 """Wrapper for list or dict to support legacy template
30 """Wrapper for list or dict to support legacy template
31
31
32 This class allows us to handle both:
32 This class allows us to handle both:
33 - "{files}" (legacy command-line-specific list hack) and
33 - "{files}" (legacy command-line-specific list hack) and
34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
35 and to access raw values:
35 and to access raw values:
36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
37 - "{get(extras, key)}"
37 - "{get(extras, key)}"
38 - "{files|json}"
38 - "{files|json}"
39 """
39 """
40
40
41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
42 if gen is not None:
42 if gen is not None:
43 self.gen = gen # generator or function returning generator
43 self.gen = gen # generator or function returning generator
44 self._values = values
44 self._values = values
45 self._makemap = makemap
45 self._makemap = makemap
46 self.joinfmt = joinfmt
46 self.joinfmt = joinfmt
47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
48 def gen(self):
48 def gen(self):
49 """Default generator to stringify this as {join(self, ' ')}"""
49 """Default generator to stringify this as {join(self, ' ')}"""
50 for i, x in enumerate(self._values):
50 for i, x in enumerate(self._values):
51 if i > 0:
51 if i > 0:
52 yield ' '
52 yield ' '
53 yield self.joinfmt(x)
53 yield self.joinfmt(x)
54 def itermaps(self):
54 def itermaps(self):
55 makemap = self._makemap
55 makemap = self._makemap
56 for x in self._values:
56 for x in self._values:
57 yield makemap(x)
57 yield makemap(x)
58 def __contains__(self, x):
58 def __contains__(self, x):
59 return x in self._values
59 return x in self._values
60 def __getitem__(self, key):
60 def __getitem__(self, key):
61 return self._values[key]
61 return self._values[key]
62 def __len__(self):
62 def __len__(self):
63 return len(self._values)
63 return len(self._values)
64 def __iter__(self):
64 def __iter__(self):
65 return iter(self._values)
65 return iter(self._values)
66 def __getattr__(self, name):
66 def __getattr__(self, name):
67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
68 r'itervalues', r'keys', r'values'):
68 r'itervalues', r'keys', r'values'):
69 raise AttributeError(name)
69 raise AttributeError(name)
70 return getattr(self._values, name)
70 return getattr(self._values, name)
71
71
72 class _mappable(object):
72 class _mappable(object):
73 """Wrapper for non-list/dict object to support map operation
73 """Wrapper for non-list/dict object to support map operation
74
74
75 This class allows us to handle both:
75 This class allows us to handle both:
76 - "{manifest}"
76 - "{manifest}"
77 - "{manifest % '{rev}:{node}'}"
77 - "{manifest % '{rev}:{node}'}"
78 - "{manifest.rev}"
78 - "{manifest.rev}"
79
79
80 Unlike a _hybrid, this does not simulate the behavior of the underling
80 Unlike a _hybrid, this does not simulate the behavior of the underling
81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
82 """
82 """
83
83
84 def __init__(self, gen, key, value, makemap):
84 def __init__(self, gen, key, value, makemap):
85 if gen is not None:
85 if gen is not None:
86 self.gen = gen # generator or function returning generator
86 self.gen = gen # generator or function returning generator
87 self._key = key
87 self._key = key
88 self._value = value # may be generator of strings
88 self._value = value # may be generator of strings
89 self._makemap = makemap
89 self._makemap = makemap
90
90
91 def gen(self):
91 def gen(self):
92 yield pycompat.bytestr(self._value)
92 yield pycompat.bytestr(self._value)
93
93
94 def tomap(self):
94 def tomap(self):
95 return self._makemap(self._key)
95 return self._makemap(self._key)
96
96
97 def itermaps(self):
97 def itermaps(self):
98 yield self.tomap()
98 yield self.tomap()
99
99
100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
101 """Wrap data to support both dict-like and string-like operations"""
101 """Wrap data to support both dict-like and string-like operations"""
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 % (k, data[k]))
103 lambda k: fmt % (k, data[k]))
104
104
105 def hybridlist(data, name, fmt='%s', gen=None):
105 def hybridlist(data, name, fmt='%s', 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 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
107 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
108
108
109 def unwraphybrid(thing):
109 def unwraphybrid(thing):
110 """Return an object which can be stringified possibly by using a legacy
110 """Return an object which can be stringified possibly by using a legacy
111 template"""
111 template"""
112 gen = getattr(thing, 'gen', None)
112 gen = getattr(thing, 'gen', None)
113 if gen is None:
113 if gen is None:
114 return thing
114 return thing
115 if callable(gen):
115 if callable(gen):
116 return gen()
116 return gen()
117 return gen
117 return gen
118
118
119 def unwrapvalue(thing):
119 def unwrapvalue(thing):
120 """Move the inner value object out of the wrapper"""
120 """Move the inner value object out of the wrapper"""
121 if not util.safehasattr(thing, '_value'):
121 if not util.safehasattr(thing, '_value'):
122 return thing
122 return thing
123 return thing._value
123 return thing._value
124
124
125 def wraphybridvalue(container, key, value):
125 def wraphybridvalue(container, key, value):
126 """Wrap an element of hybrid container to be mappable
126 """Wrap an element of hybrid container to be mappable
127
127
128 The key is passed to the makemap function of the given container, which
128 The key is passed to the makemap function of the given container, which
129 should be an item generated by iter(container).
129 should be an item generated by iter(container).
130 """
130 """
131 makemap = getattr(container, '_makemap', None)
131 makemap = getattr(container, '_makemap', None)
132 if makemap is None:
132 if makemap is None:
133 return value
133 return value
134 if util.safehasattr(value, '_makemap'):
134 if util.safehasattr(value, '_makemap'):
135 # a nested hybrid list/dict, which has its own way of map operation
135 # a nested hybrid list/dict, which has its own way of map operation
136 return value
136 return value
137 return _mappable(None, key, value, makemap)
137 return _mappable(None, key, value, makemap)
138
138
139 def compatdict(context, mapping, name, data, key='key', value='value',
139 def compatdict(context, mapping, name, data, key='key', value='value',
140 fmt='%s=%s', plural=None, separator=' '):
140 fmt='%s=%s', plural=None, separator=' '):
141 """Wrap data like hybriddict(), but also supports old-style list template
141 """Wrap data like hybriddict(), but also supports old-style list template
142
142
143 This exists for backward compatibility with the old-style template. Use
143 This exists for backward compatibility with the old-style template. Use
144 hybriddict() for new template keywords.
144 hybriddict() for new template keywords.
145 """
145 """
146 c = [{key: k, value: v} for k, v in data.iteritems()]
146 c = [{key: k, value: v} for k, v in data.iteritems()]
147 t = context.resource(mapping, 'templ')
147 t = context.resource(mapping, 'templ')
148 f = _showlist(name, c, t, mapping, plural, separator)
148 f = _showlist(name, c, t, mapping, plural, separator)
149 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
149 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
150
150
151 def compatlist(context, mapping, name, data, element=None, fmt='%s',
151 def compatlist(context, mapping, name, data, element=None, fmt='%s',
152 plural=None, separator=' '):
152 plural=None, separator=' '):
153 """Wrap data like hybridlist(), but also supports old-style list template
153 """Wrap data like hybridlist(), but also supports old-style list template
154
154
155 This exists for backward compatibility with the old-style template. Use
155 This exists for backward compatibility with the old-style template. Use
156 hybridlist() for new template keywords.
156 hybridlist() for new template keywords.
157 """
157 """
158 t = context.resource(mapping, 'templ')
158 t = context.resource(mapping, 'templ')
159 f = _showlist(name, data, t, mapping, plural, separator)
159 f = _showlist(name, data, t, mapping, plural, separator)
160 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
160 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
161
161
162 def showdict(name, data, mapping, plural=None, key='key', value='value',
162 def showdict(name, data, mapping, plural=None, key='key', value='value',
163 fmt='%s=%s', separator=' '):
163 fmt='%s=%s', separator=' '):
164 c = [{key: k, value: v} for k, v in data.iteritems()]
164 c = [{key: k, value: v} for k, v in data.iteritems()]
165 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
165 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
166 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
166 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
167
167
168 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
168 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
169 if not element:
169 if not element:
170 element = name
170 element = name
171 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
171 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
172 return hybridlist(values, name=element, gen=f)
172 return hybridlist(values, name=element, gen=f)
173
173
174 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
174 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
175 '''expand set of values.
175 '''expand set of values.
176 name is name of key in template map.
176 name is name of key in template map.
177 values is list of strings or dicts.
177 values is list of strings or dicts.
178 plural is plural of name, if not simply name + 's'.
178 plural is plural of name, if not simply name + 's'.
179 separator is used to join values as a string
179 separator is used to join values as a string
180
180
181 expansion works like this, given name 'foo'.
181 expansion works like this, given name 'foo'.
182
182
183 if values is empty, expand 'no_foos'.
183 if values is empty, expand 'no_foos'.
184
184
185 if 'foo' not in template map, return values as a string,
185 if 'foo' not in template map, return values as a string,
186 joined by 'separator'.
186 joined by 'separator'.
187
187
188 expand 'start_foos'.
188 expand 'start_foos'.
189
189
190 for each value, expand 'foo'. if 'last_foo' in template
190 for each value, expand 'foo'. if 'last_foo' in template
191 map, expand it instead of 'foo' for last key.
191 map, expand it instead of 'foo' for last key.
192
192
193 expand 'end_foos'.
193 expand 'end_foos'.
194 '''
194 '''
195 strmapping = pycompat.strkwargs(mapping)
195 strmapping = pycompat.strkwargs(mapping)
196 if not plural:
196 if not plural:
197 plural = name + 's'
197 plural = name + 's'
198 if not values:
198 if not values:
199 noname = 'no_' + plural
199 noname = 'no_' + plural
200 if noname in templ:
200 if noname in templ:
201 yield templ(noname, **strmapping)
201 yield templ(noname, **strmapping)
202 return
202 return
203 if name not in templ:
203 if name not in templ:
204 if isinstance(values[0], bytes):
204 if isinstance(values[0], bytes):
205 yield separator.join(values)
205 yield separator.join(values)
206 else:
206 else:
207 for v in values:
207 for v in values:
208 r = dict(v)
208 r = dict(v)
209 r.update(mapping)
209 r.update(mapping)
210 yield r
210 yield r
211 return
211 return
212 startname = 'start_' + plural
212 startname = 'start_' + plural
213 if startname in templ:
213 if startname in templ:
214 yield templ(startname, **strmapping)
214 yield templ(startname, **strmapping)
215 vmapping = mapping.copy()
215 vmapping = mapping.copy()
216 def one(v, tag=name):
216 def one(v, tag=name):
217 try:
217 try:
218 vmapping.update(v)
218 vmapping.update(v)
219 # Python 2 raises ValueError if the type of v is wrong. Python
219 # Python 2 raises ValueError if the type of v is wrong. Python
220 # 3 raises TypeError.
220 # 3 raises TypeError.
221 except (AttributeError, TypeError, ValueError):
221 except (AttributeError, TypeError, ValueError):
222 try:
222 try:
223 # Python 2 raises ValueError trying to destructure an e.g.
223 # Python 2 raises ValueError trying to destructure an e.g.
224 # bytes. Python 3 raises TypeError.
224 # bytes. Python 3 raises TypeError.
225 for a, b in v:
225 for a, b in v:
226 vmapping[a] = b
226 vmapping[a] = b
227 except (TypeError, ValueError):
227 except (TypeError, ValueError):
228 vmapping[name] = v
228 vmapping[name] = v
229 return templ(tag, **pycompat.strkwargs(vmapping))
229 return templ(tag, **pycompat.strkwargs(vmapping))
230 lastname = 'last_' + name
230 lastname = 'last_' + name
231 if lastname in templ:
231 if lastname in templ:
232 last = values.pop()
232 last = values.pop()
233 else:
233 else:
234 last = None
234 last = None
235 for v in values:
235 for v in values:
236 yield one(v)
236 yield one(v)
237 if last is not None:
237 if last is not None:
238 yield one(last, tag=lastname)
238 yield one(last, tag=lastname)
239 endname = 'end_' + plural
239 endname = 'end_' + plural
240 if endname in templ:
240 if endname in templ:
241 yield templ(endname, **strmapping)
241 yield templ(endname, **strmapping)
242
242
243 def getlatesttags(context, mapping, pattern=None):
243 def getlatesttags(context, mapping, pattern=None):
244 '''return date, distance and name for the latest tag of rev'''
244 '''return date, distance and name for the latest tag of rev'''
245 repo = context.resource(mapping, 'repo')
245 repo = context.resource(mapping, 'repo')
246 ctx = context.resource(mapping, 'ctx')
246 ctx = context.resource(mapping, 'ctx')
247 cache = context.resource(mapping, 'cache')
247 cache = context.resource(mapping, 'cache')
248
248
249 cachename = 'latesttags'
249 cachename = 'latesttags'
250 if pattern is not None:
250 if pattern is not None:
251 cachename += '-' + pattern
251 cachename += '-' + pattern
252 match = util.stringmatcher(pattern)[2]
252 match = util.stringmatcher(pattern)[2]
253 else:
253 else:
254 match = util.always
254 match = util.always
255
255
256 if cachename not in cache:
256 if cachename not in cache:
257 # Cache mapping from rev to a tuple with tag date, tag
257 # Cache mapping from rev to a tuple with tag date, tag
258 # distance and tag name
258 # distance and tag name
259 cache[cachename] = {-1: (0, 0, ['null'])}
259 cache[cachename] = {-1: (0, 0, ['null'])}
260 latesttags = cache[cachename]
260 latesttags = cache[cachename]
261
261
262 rev = ctx.rev()
262 rev = ctx.rev()
263 todo = [rev]
263 todo = [rev]
264 while todo:
264 while todo:
265 rev = todo.pop()
265 rev = todo.pop()
266 if rev in latesttags:
266 if rev in latesttags:
267 continue
267 continue
268 ctx = repo[rev]
268 ctx = repo[rev]
269 tags = [t for t in ctx.tags()
269 tags = [t for t in ctx.tags()
270 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
270 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
271 and match(t))]
271 and match(t))]
272 if tags:
272 if tags:
273 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
273 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
274 continue
274 continue
275 try:
275 try:
276 ptags = [latesttags[p.rev()] for p in ctx.parents()]
276 ptags = [latesttags[p.rev()] for p in ctx.parents()]
277 if len(ptags) > 1:
277 if len(ptags) > 1:
278 if ptags[0][2] == ptags[1][2]:
278 if ptags[0][2] == ptags[1][2]:
279 # The tuples are laid out so the right one can be found by
279 # The tuples are laid out so the right one can be found by
280 # comparison in this case.
280 # comparison in this case.
281 pdate, pdist, ptag = max(ptags)
281 pdate, pdist, ptag = max(ptags)
282 else:
282 else:
283 def key(x):
283 def key(x):
284 changessincetag = len(repo.revs('only(%d, %s)',
284 changessincetag = len(repo.revs('only(%d, %s)',
285 ctx.rev(), x[2][0]))
285 ctx.rev(), x[2][0]))
286 # Smallest number of changes since tag wins. Date is
286 # Smallest number of changes since tag wins. Date is
287 # used as tiebreaker.
287 # used as tiebreaker.
288 return [-changessincetag, x[0]]
288 return [-changessincetag, x[0]]
289 pdate, pdist, ptag = max(ptags, key=key)
289 pdate, pdist, ptag = max(ptags, key=key)
290 else:
290 else:
291 pdate, pdist, ptag = ptags[0]
291 pdate, pdist, ptag = ptags[0]
292 except KeyError:
292 except KeyError:
293 # Cache miss - recurse
293 # Cache miss - recurse
294 todo.append(rev)
294 todo.append(rev)
295 todo.extend(p.rev() for p in ctx.parents())
295 todo.extend(p.rev() for p in ctx.parents())
296 continue
296 continue
297 latesttags[rev] = pdate, pdist + 1, ptag
297 latesttags[rev] = pdate, pdist + 1, ptag
298 return latesttags[rev]
298 return latesttags[rev]
299
299
300 def getrenamedfn(repo, endrev=None):
300 def getrenamedfn(repo, endrev=None):
301 rcache = {}
301 rcache = {}
302 if endrev is None:
302 if endrev is None:
303 endrev = len(repo)
303 endrev = len(repo)
304
304
305 def getrenamed(fn, rev):
305 def getrenamed(fn, rev):
306 '''looks up all renames for a file (up to endrev) the first
306 '''looks up all renames for a file (up to endrev) the first
307 time the file is given. It indexes on the changerev and only
307 time the file is given. It indexes on the changerev and only
308 parses the manifest if linkrev != changerev.
308 parses the manifest if linkrev != changerev.
309 Returns rename info for fn at changerev rev.'''
309 Returns rename info for fn at changerev rev.'''
310 if fn not in rcache:
310 if fn not in rcache:
311 rcache[fn] = {}
311 rcache[fn] = {}
312 fl = repo.file(fn)
312 fl = repo.file(fn)
313 for i in fl:
313 for i in fl:
314 lr = fl.linkrev(i)
314 lr = fl.linkrev(i)
315 renamed = fl.renamed(fl.node(i))
315 renamed = fl.renamed(fl.node(i))
316 rcache[fn][lr] = renamed
316 rcache[fn][lr] = renamed
317 if lr >= endrev:
317 if lr >= endrev:
318 break
318 break
319 if rev in rcache[fn]:
319 if rev in rcache[fn]:
320 return rcache[fn][rev]
320 return rcache[fn][rev]
321
321
322 # If linkrev != rev (i.e. rev not found in rcache) fallback to
322 # If linkrev != rev (i.e. rev not found in rcache) fallback to
323 # filectx logic.
323 # filectx logic.
324 try:
324 try:
325 return repo[rev][fn].renamed()
325 return repo[rev][fn].renamed()
326 except error.LookupError:
326 except error.LookupError:
327 return None
327 return None
328
328
329 return getrenamed
329 return getrenamed
330
330
331 def getlogcolumns():
331 def getlogcolumns():
332 """Return a dict of log column labels"""
332 """Return a dict of log column labels"""
333 _ = pycompat.identity # temporarily disable gettext
333 _ = pycompat.identity # temporarily disable gettext
334 # i18n: column positioning for "hg log"
334 # i18n: column positioning for "hg log"
335 columns = _('bookmark: %s\n'
335 columns = _('bookmark: %s\n'
336 'branch: %s\n'
336 'branch: %s\n'
337 'changeset: %s\n'
337 'changeset: %s\n'
338 'copies: %s\n'
338 'copies: %s\n'
339 'date: %s\n'
339 'date: %s\n'
340 'extra: %s=%s\n'
340 'extra: %s=%s\n'
341 'files+: %s\n'
341 'files+: %s\n'
342 'files-: %s\n'
342 'files-: %s\n'
343 'files: %s\n'
343 'files: %s\n'
344 'instability: %s\n'
344 'instability: %s\n'
345 'manifest: %s\n'
345 'manifest: %s\n'
346 'obsolete: %s\n'
346 'obsolete: %s\n'
347 'parent: %s\n'
347 'parent: %s\n'
348 'phase: %s\n'
348 'phase: %s\n'
349 'summary: %s\n'
349 'summary: %s\n'
350 'tag: %s\n'
350 'tag: %s\n'
351 'user: %s\n')
351 'user: %s\n')
352 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
352 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
353 i18n._(columns).splitlines(True)))
353 i18n._(columns).splitlines(True)))
354
354
355 # default templates internally used for rendering of lists
355 # default templates internally used for rendering of lists
356 defaulttempl = {
356 defaulttempl = {
357 'parent': '{rev}:{node|formatnode} ',
357 'parent': '{rev}:{node|formatnode} ',
358 'manifest': '{rev}:{node|formatnode}',
358 'manifest': '{rev}:{node|formatnode}',
359 'file_copy': '{name} ({source})',
359 'file_copy': '{name} ({source})',
360 'envvar': '{key}={value}',
360 'envvar': '{key}={value}',
361 'extra': '{key}={value|stringescape}'
361 'extra': '{key}={value|stringescape}'
362 }
362 }
363 # filecopy is preserved for compatibility reasons
363 # filecopy is preserved for compatibility reasons
364 defaulttempl['filecopy'] = defaulttempl['file_copy']
364 defaulttempl['filecopy'] = defaulttempl['file_copy']
365
365
366 # keywords are callables (see registrar.templatekeyword for details)
366 # keywords are callables (see registrar.templatekeyword for details)
367 keywords = {}
367 keywords = {}
368 templatekeyword = registrar.templatekeyword(keywords)
368 templatekeyword = registrar.templatekeyword(keywords)
369
369
370 @templatekeyword('author', requires={'ctx'})
370 @templatekeyword('author', requires={'ctx'})
371 def showauthor(context, mapping):
371 def showauthor(context, mapping):
372 """String. The unmodified author of the changeset."""
372 """String. The unmodified author of the changeset."""
373 ctx = context.resource(mapping, 'ctx')
373 ctx = context.resource(mapping, 'ctx')
374 return ctx.user()
374 return ctx.user()
375
375
376 @templatekeyword('bisect', requires={'repo', 'ctx'})
376 @templatekeyword('bisect', requires={'repo', 'ctx'})
377 def showbisect(context, mapping):
377 def showbisect(context, mapping):
378 """String. The changeset bisection status."""
378 """String. The changeset bisection status."""
379 repo = context.resource(mapping, 'repo')
379 repo = context.resource(mapping, 'repo')
380 ctx = context.resource(mapping, 'ctx')
380 ctx = context.resource(mapping, 'ctx')
381 return hbisect.label(repo, ctx.node())
381 return hbisect.label(repo, ctx.node())
382
382
383 @templatekeyword('branch', requires={'ctx'})
383 @templatekeyword('branch', requires={'ctx'})
384 def showbranch(context, mapping):
384 def showbranch(context, mapping):
385 """String. The name of the branch on which the changeset was
385 """String. The name of the branch on which the changeset was
386 committed.
386 committed.
387 """
387 """
388 ctx = context.resource(mapping, 'ctx')
388 ctx = context.resource(mapping, 'ctx')
389 return ctx.branch()
389 return ctx.branch()
390
390
391 @templatekeyword('branches', requires={'ctx', 'templ'})
391 @templatekeyword('branches', requires={'ctx', 'templ'})
392 def showbranches(context, mapping):
392 def showbranches(context, mapping):
393 """List of strings. The name of the branch on which the
393 """List of strings. The name of the branch on which the
394 changeset was committed. Will be empty if the branch name was
394 changeset was committed. Will be empty if the branch name was
395 default. (DEPRECATED)
395 default. (DEPRECATED)
396 """
396 """
397 ctx = context.resource(mapping, 'ctx')
397 ctx = context.resource(mapping, 'ctx')
398 branch = ctx.branch()
398 branch = ctx.branch()
399 if branch != 'default':
399 if branch != 'default':
400 return compatlist(context, mapping, 'branch', [branch],
400 return compatlist(context, mapping, 'branch', [branch],
401 plural='branches')
401 plural='branches')
402 return compatlist(context, mapping, 'branch', [], plural='branches')
402 return compatlist(context, mapping, 'branch', [], plural='branches')
403
403
404 @templatekeyword('bookmarks')
404 @templatekeyword('bookmarks', requires={'repo', 'ctx', 'templ'})
405 def showbookmarks(**args):
405 def showbookmarks(context, mapping):
406 """List of strings. Any bookmarks associated with the
406 """List of strings. Any bookmarks associated with the
407 changeset. Also sets 'active', the name of the active bookmark.
407 changeset. Also sets 'active', the name of the active bookmark.
408 """
408 """
409 args = pycompat.byteskwargs(args)
409 repo = context.resource(mapping, 'repo')
410 repo = args['ctx']._repo
410 ctx = context.resource(mapping, 'ctx')
411 bookmarks = args['ctx'].bookmarks()
411 templ = context.resource(mapping, 'templ')
412 bookmarks = ctx.bookmarks()
412 active = repo._activebookmark
413 active = repo._activebookmark
413 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
414 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
414 f = _showlist('bookmark', bookmarks, args['templ'], args)
415 f = _showlist('bookmark', bookmarks, templ, mapping)
415 return _hybrid(f, bookmarks, makemap, pycompat.identity)
416 return _hybrid(f, bookmarks, makemap, pycompat.identity)
416
417
417 @templatekeyword('children', requires={'ctx', 'templ'})
418 @templatekeyword('children', requires={'ctx', 'templ'})
418 def showchildren(context, mapping):
419 def showchildren(context, mapping):
419 """List of strings. The children of the changeset."""
420 """List of strings. The children of the changeset."""
420 ctx = context.resource(mapping, 'ctx')
421 ctx = context.resource(mapping, 'ctx')
421 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
422 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
422 return compatlist(context, mapping, 'children', childrevs, element='child')
423 return compatlist(context, mapping, 'children', childrevs, element='child')
423
424
424 # Deprecated, but kept alive for help generation a purpose.
425 # Deprecated, but kept alive for help generation a purpose.
425 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
426 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
426 def showcurrentbookmark(context, mapping):
427 def showcurrentbookmark(context, mapping):
427 """String. The active bookmark, if it is associated with the changeset.
428 """String. The active bookmark, if it is associated with the changeset.
428 (DEPRECATED)"""
429 (DEPRECATED)"""
429 return showactivebookmark(context, mapping)
430 return showactivebookmark(context, mapping)
430
431
431 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
432 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
432 def showactivebookmark(context, mapping):
433 def showactivebookmark(context, mapping):
433 """String. The active bookmark, if it is associated with the changeset."""
434 """String. The active bookmark, if it is associated with the changeset."""
434 repo = context.resource(mapping, 'repo')
435 repo = context.resource(mapping, 'repo')
435 ctx = context.resource(mapping, 'ctx')
436 ctx = context.resource(mapping, 'ctx')
436 active = repo._activebookmark
437 active = repo._activebookmark
437 if active and active in ctx.bookmarks():
438 if active and active in ctx.bookmarks():
438 return active
439 return active
439 return ''
440 return ''
440
441
441 @templatekeyword('date', requires={'ctx'})
442 @templatekeyword('date', requires={'ctx'})
442 def showdate(context, mapping):
443 def showdate(context, mapping):
443 """Date information. The date when the changeset was committed."""
444 """Date information. The date when the changeset was committed."""
444 ctx = context.resource(mapping, 'ctx')
445 ctx = context.resource(mapping, 'ctx')
445 return ctx.date()
446 return ctx.date()
446
447
447 @templatekeyword('desc', requires={'ctx'})
448 @templatekeyword('desc', requires={'ctx'})
448 def showdescription(context, mapping):
449 def showdescription(context, mapping):
449 """String. The text of the changeset description."""
450 """String. The text of the changeset description."""
450 ctx = context.resource(mapping, 'ctx')
451 ctx = context.resource(mapping, 'ctx')
451 s = ctx.description()
452 s = ctx.description()
452 if isinstance(s, encoding.localstr):
453 if isinstance(s, encoding.localstr):
453 # try hard to preserve utf-8 bytes
454 # try hard to preserve utf-8 bytes
454 return encoding.tolocal(encoding.fromlocal(s).strip())
455 return encoding.tolocal(encoding.fromlocal(s).strip())
455 else:
456 else:
456 return s.strip()
457 return s.strip()
457
458
458 @templatekeyword('diffstat', requires={'ctx'})
459 @templatekeyword('diffstat', requires={'ctx'})
459 def showdiffstat(context, mapping):
460 def showdiffstat(context, mapping):
460 """String. Statistics of changes with the following format:
461 """String. Statistics of changes with the following format:
461 "modified files: +added/-removed lines"
462 "modified files: +added/-removed lines"
462 """
463 """
463 ctx = context.resource(mapping, 'ctx')
464 ctx = context.resource(mapping, 'ctx')
464 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
465 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
465 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
466 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
466 return '%d: +%d/-%d' % (len(stats), adds, removes)
467 return '%d: +%d/-%d' % (len(stats), adds, removes)
467
468
468 @templatekeyword('envvars', requires={'ui', 'templ'})
469 @templatekeyword('envvars', requires={'ui', 'templ'})
469 def showenvvars(context, mapping):
470 def showenvvars(context, mapping):
470 """A dictionary of environment variables. (EXPERIMENTAL)"""
471 """A dictionary of environment variables. (EXPERIMENTAL)"""
471 ui = context.resource(mapping, 'ui')
472 ui = context.resource(mapping, 'ui')
472 env = ui.exportableenviron()
473 env = ui.exportableenviron()
473 env = util.sortdict((k, env[k]) for k in sorted(env))
474 env = util.sortdict((k, env[k]) for k in sorted(env))
474 return compatdict(context, mapping, 'envvar', env, plural='envvars')
475 return compatdict(context, mapping, 'envvar', env, plural='envvars')
475
476
476 @templatekeyword('extras')
477 @templatekeyword('extras', requires={'ctx', 'templ'})
477 def showextras(**args):
478 def showextras(context, mapping):
478 """List of dicts with key, value entries of the 'extras'
479 """List of dicts with key, value entries of the 'extras'
479 field of this changeset."""
480 field of this changeset."""
480 args = pycompat.byteskwargs(args)
481 ctx = context.resource(mapping, 'ctx')
481 extras = args['ctx'].extra()
482 templ = context.resource(mapping, 'templ')
483 extras = ctx.extra()
482 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
484 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
483 makemap = lambda k: {'key': k, 'value': extras[k]}
485 makemap = lambda k: {'key': k, 'value': extras[k]}
484 c = [makemap(k) for k in extras]
486 c = [makemap(k) for k in extras]
485 f = _showlist('extra', c, args['templ'], args, plural='extras')
487 f = _showlist('extra', c, templ, mapping, plural='extras')
486 return _hybrid(f, extras, makemap,
488 return _hybrid(f, extras, makemap,
487 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
489 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
488
490
489 def _showfilesbystat(context, mapping, name, index):
491 def _showfilesbystat(context, mapping, name, index):
490 repo = context.resource(mapping, 'repo')
492 repo = context.resource(mapping, 'repo')
491 ctx = context.resource(mapping, 'ctx')
493 ctx = context.resource(mapping, 'ctx')
492 revcache = context.resource(mapping, 'revcache')
494 revcache = context.resource(mapping, 'revcache')
493 if 'files' not in revcache:
495 if 'files' not in revcache:
494 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
496 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
495 files = revcache['files'][index]
497 files = revcache['files'][index]
496 return compatlist(context, mapping, name, files, element='file')
498 return compatlist(context, mapping, name, files, element='file')
497
499
498 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
500 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
499 def showfileadds(context, mapping):
501 def showfileadds(context, mapping):
500 """List of strings. Files added by this changeset."""
502 """List of strings. Files added by this changeset."""
501 return _showfilesbystat(context, mapping, 'file_add', 1)
503 return _showfilesbystat(context, mapping, 'file_add', 1)
502
504
503 @templatekeyword('file_copies',
505 @templatekeyword('file_copies',
504 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
506 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
505 def showfilecopies(context, mapping):
507 def showfilecopies(context, mapping):
506 """List of strings. Files copied in this changeset with
508 """List of strings. Files copied in this changeset with
507 their sources.
509 their sources.
508 """
510 """
509 repo = context.resource(mapping, 'repo')
511 repo = context.resource(mapping, 'repo')
510 ctx = context.resource(mapping, 'ctx')
512 ctx = context.resource(mapping, 'ctx')
511 cache = context.resource(mapping, 'cache')
513 cache = context.resource(mapping, 'cache')
512 copies = context.resource(mapping, 'revcache').get('copies')
514 copies = context.resource(mapping, 'revcache').get('copies')
513 if copies is None:
515 if copies is None:
514 if 'getrenamed' not in cache:
516 if 'getrenamed' not in cache:
515 cache['getrenamed'] = getrenamedfn(repo)
517 cache['getrenamed'] = getrenamedfn(repo)
516 copies = []
518 copies = []
517 getrenamed = cache['getrenamed']
519 getrenamed = cache['getrenamed']
518 for fn in ctx.files():
520 for fn in ctx.files():
519 rename = getrenamed(fn, ctx.rev())
521 rename = getrenamed(fn, ctx.rev())
520 if rename:
522 if rename:
521 copies.append((fn, rename[0]))
523 copies.append((fn, rename[0]))
522
524
523 copies = util.sortdict(copies)
525 copies = util.sortdict(copies)
524 return compatdict(context, mapping, 'file_copy', copies,
526 return compatdict(context, mapping, 'file_copy', copies,
525 key='name', value='source', fmt='%s (%s)',
527 key='name', value='source', fmt='%s (%s)',
526 plural='file_copies')
528 plural='file_copies')
527
529
528 # showfilecopiesswitch() displays file copies only if copy records are
530 # showfilecopiesswitch() displays file copies only if copy records are
529 # provided before calling the templater, usually with a --copies
531 # provided before calling the templater, usually with a --copies
530 # command line switch.
532 # command line switch.
531 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
533 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
532 def showfilecopiesswitch(context, mapping):
534 def showfilecopiesswitch(context, mapping):
533 """List of strings. Like "file_copies" but displayed
535 """List of strings. Like "file_copies" but displayed
534 only if the --copied switch is set.
536 only if the --copied switch is set.
535 """
537 """
536 copies = context.resource(mapping, 'revcache').get('copies') or []
538 copies = context.resource(mapping, 'revcache').get('copies') or []
537 copies = util.sortdict(copies)
539 copies = util.sortdict(copies)
538 return compatdict(context, mapping, 'file_copy', copies,
540 return compatdict(context, mapping, 'file_copy', copies,
539 key='name', value='source', fmt='%s (%s)',
541 key='name', value='source', fmt='%s (%s)',
540 plural='file_copies')
542 plural='file_copies')
541
543
542 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
544 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
543 def showfiledels(context, mapping):
545 def showfiledels(context, mapping):
544 """List of strings. Files removed by this changeset."""
546 """List of strings. Files removed by this changeset."""
545 return _showfilesbystat(context, mapping, 'file_del', 2)
547 return _showfilesbystat(context, mapping, 'file_del', 2)
546
548
547 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
549 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
548 def showfilemods(context, mapping):
550 def showfilemods(context, mapping):
549 """List of strings. Files modified by this changeset."""
551 """List of strings. Files modified by this changeset."""
550 return _showfilesbystat(context, mapping, 'file_mod', 0)
552 return _showfilesbystat(context, mapping, 'file_mod', 0)
551
553
552 @templatekeyword('files', requires={'ctx', 'templ'})
554 @templatekeyword('files', requires={'ctx', 'templ'})
553 def showfiles(context, mapping):
555 def showfiles(context, mapping):
554 """List of strings. All files modified, added, or removed by this
556 """List of strings. All files modified, added, or removed by this
555 changeset.
557 changeset.
556 """
558 """
557 ctx = context.resource(mapping, 'ctx')
559 ctx = context.resource(mapping, 'ctx')
558 return compatlist(context, mapping, 'file', ctx.files())
560 return compatlist(context, mapping, 'file', ctx.files())
559
561
560 @templatekeyword('graphnode', requires={'repo', 'ctx'})
562 @templatekeyword('graphnode', requires={'repo', 'ctx'})
561 def showgraphnode(context, mapping):
563 def showgraphnode(context, mapping):
562 """String. The character representing the changeset node in an ASCII
564 """String. The character representing the changeset node in an ASCII
563 revision graph."""
565 revision graph."""
564 repo = context.resource(mapping, 'repo')
566 repo = context.resource(mapping, 'repo')
565 ctx = context.resource(mapping, 'ctx')
567 ctx = context.resource(mapping, 'ctx')
566 return getgraphnode(repo, ctx)
568 return getgraphnode(repo, ctx)
567
569
568 def getgraphnode(repo, ctx):
570 def getgraphnode(repo, ctx):
569 wpnodes = repo.dirstate.parents()
571 wpnodes = repo.dirstate.parents()
570 if wpnodes[1] == nullid:
572 if wpnodes[1] == nullid:
571 wpnodes = wpnodes[:1]
573 wpnodes = wpnodes[:1]
572 if ctx.node() in wpnodes:
574 if ctx.node() in wpnodes:
573 return '@'
575 return '@'
574 elif ctx.obsolete():
576 elif ctx.obsolete():
575 return 'x'
577 return 'x'
576 elif ctx.isunstable():
578 elif ctx.isunstable():
577 return '*'
579 return '*'
578 elif ctx.closesbranch():
580 elif ctx.closesbranch():
579 return '_'
581 return '_'
580 else:
582 else:
581 return 'o'
583 return 'o'
582
584
583 @templatekeyword('graphwidth', requires=())
585 @templatekeyword('graphwidth', requires=())
584 def showgraphwidth(context, mapping):
586 def showgraphwidth(context, mapping):
585 """Integer. The width of the graph drawn by 'log --graph' or zero."""
587 """Integer. The width of the graph drawn by 'log --graph' or zero."""
586 # just hosts documentation; should be overridden by template mapping
588 # just hosts documentation; should be overridden by template mapping
587 return 0
589 return 0
588
590
589 @templatekeyword('index', requires=())
591 @templatekeyword('index', requires=())
590 def showindex(context, mapping):
592 def showindex(context, mapping):
591 """Integer. The current iteration of the loop. (0 indexed)"""
593 """Integer. The current iteration of the loop. (0 indexed)"""
592 # just hosts documentation; should be overridden by template mapping
594 # just hosts documentation; should be overridden by template mapping
593 raise error.Abort(_("can't use index in this context"))
595 raise error.Abort(_("can't use index in this context"))
594
596
595 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
597 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
596 def showlatesttag(context, mapping):
598 def showlatesttag(context, mapping):
597 """List of strings. The global tags on the most recent globally
599 """List of strings. The global tags on the most recent globally
598 tagged ancestor of this changeset. If no such tags exist, the list
600 tagged ancestor of this changeset. If no such tags exist, the list
599 consists of the single string "null".
601 consists of the single string "null".
600 """
602 """
601 return showlatesttags(context, mapping, None)
603 return showlatesttags(context, mapping, None)
602
604
603 def showlatesttags(context, mapping, pattern):
605 def showlatesttags(context, mapping, pattern):
604 """helper method for the latesttag keyword and function"""
606 """helper method for the latesttag keyword and function"""
605 latesttags = getlatesttags(context, mapping, pattern)
607 latesttags = getlatesttags(context, mapping, pattern)
606
608
607 # latesttag[0] is an implementation detail for sorting csets on different
609 # latesttag[0] is an implementation detail for sorting csets on different
608 # branches in a stable manner- it is the date the tagged cset was created,
610 # branches in a stable manner- it is the date the tagged cset was created,
609 # not the date the tag was created. Therefore it isn't made visible here.
611 # not the date the tag was created. Therefore it isn't made visible here.
610 makemap = lambda v: {
612 makemap = lambda v: {
611 'changes': _showchangessincetag,
613 'changes': _showchangessincetag,
612 'distance': latesttags[1],
614 'distance': latesttags[1],
613 'latesttag': v, # BC with {latesttag % '{latesttag}'}
615 'latesttag': v, # BC with {latesttag % '{latesttag}'}
614 'tag': v
616 'tag': v
615 }
617 }
616
618
617 tags = latesttags[2]
619 tags = latesttags[2]
618 templ = context.resource(mapping, 'templ')
620 templ = context.resource(mapping, 'templ')
619 f = _showlist('latesttag', tags, templ, mapping, separator=':')
621 f = _showlist('latesttag', tags, templ, mapping, separator=':')
620 return _hybrid(f, tags, makemap, pycompat.identity)
622 return _hybrid(f, tags, makemap, pycompat.identity)
621
623
622 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
624 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
623 def showlatesttagdistance(context, mapping):
625 def showlatesttagdistance(context, mapping):
624 """Integer. Longest path to the latest tag."""
626 """Integer. Longest path to the latest tag."""
625 return getlatesttags(context, mapping)[1]
627 return getlatesttags(context, mapping)[1]
626
628
627 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
629 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
628 def showchangessincelatesttag(context, mapping):
630 def showchangessincelatesttag(context, mapping):
629 """Integer. All ancestors not in the latest tag."""
631 """Integer. All ancestors not in the latest tag."""
630 mapping = mapping.copy()
632 mapping = mapping.copy()
631 mapping['tag'] = getlatesttags(context, mapping)[2][0]
633 mapping['tag'] = getlatesttags(context, mapping)[2][0]
632 return _showchangessincetag(context, mapping)
634 return _showchangessincetag(context, mapping)
633
635
634 def _showchangessincetag(context, mapping):
636 def _showchangessincetag(context, mapping):
635 repo = context.resource(mapping, 'repo')
637 repo = context.resource(mapping, 'repo')
636 ctx = context.resource(mapping, 'ctx')
638 ctx = context.resource(mapping, 'ctx')
637 offset = 0
639 offset = 0
638 revs = [ctx.rev()]
640 revs = [ctx.rev()]
639 tag = context.symbol(mapping, 'tag')
641 tag = context.symbol(mapping, 'tag')
640
642
641 # The only() revset doesn't currently support wdir()
643 # The only() revset doesn't currently support wdir()
642 if ctx.rev() is None:
644 if ctx.rev() is None:
643 offset = 1
645 offset = 1
644 revs = [p.rev() for p in ctx.parents()]
646 revs = [p.rev() for p in ctx.parents()]
645
647
646 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
648 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
647
649
648 # teach templater latesttags.changes is switched to (context, mapping) API
650 # teach templater latesttags.changes is switched to (context, mapping) API
649 _showchangessincetag._requires = {'repo', 'ctx'}
651 _showchangessincetag._requires = {'repo', 'ctx'}
650
652
651 @templatekeyword('manifest', requires={'repo', 'ctx', 'templ'})
653 @templatekeyword('manifest', requires={'repo', 'ctx', 'templ'})
652 def showmanifest(context, mapping):
654 def showmanifest(context, mapping):
653 repo = context.resource(mapping, 'repo')
655 repo = context.resource(mapping, 'repo')
654 ctx = context.resource(mapping, 'ctx')
656 ctx = context.resource(mapping, 'ctx')
655 templ = context.resource(mapping, 'templ')
657 templ = context.resource(mapping, 'templ')
656 mnode = ctx.manifestnode()
658 mnode = ctx.manifestnode()
657 if mnode is None:
659 if mnode is None:
658 # just avoid crash, we might want to use the 'ff...' hash in future
660 # just avoid crash, we might want to use the 'ff...' hash in future
659 return
661 return
660 mrev = repo.manifestlog._revlog.rev(mnode)
662 mrev = repo.manifestlog._revlog.rev(mnode)
661 mhex = hex(mnode)
663 mhex = hex(mnode)
662 mapping = mapping.copy()
664 mapping = mapping.copy()
663 mapping.update({'rev': mrev, 'node': mhex})
665 mapping.update({'rev': mrev, 'node': mhex})
664 f = templ('manifest', **pycompat.strkwargs(mapping))
666 f = templ('manifest', **pycompat.strkwargs(mapping))
665 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
667 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
666 # rev and node are completely different from changeset's.
668 # rev and node are completely different from changeset's.
667 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
669 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
668
670
669 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
671 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
670 def showobsfate(context, mapping):
672 def showobsfate(context, mapping):
671 # this function returns a list containing pre-formatted obsfate strings.
673 # this function returns a list containing pre-formatted obsfate strings.
672 #
674 #
673 # This function will be replaced by templates fragments when we will have
675 # This function will be replaced by templates fragments when we will have
674 # the verbosity templatekw available.
676 # the verbosity templatekw available.
675 succsandmarkers = showsuccsandmarkers(context, mapping)
677 succsandmarkers = showsuccsandmarkers(context, mapping)
676
678
677 ui = context.resource(mapping, 'ui')
679 ui = context.resource(mapping, 'ui')
678 values = []
680 values = []
679
681
680 for x in succsandmarkers:
682 for x in succsandmarkers:
681 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
683 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
682
684
683 return compatlist(context, mapping, "fate", values)
685 return compatlist(context, mapping, "fate", values)
684
686
685 def shownames(context, mapping, namespace):
687 def shownames(context, mapping, namespace):
686 """helper method to generate a template keyword for a namespace"""
688 """helper method to generate a template keyword for a namespace"""
687 repo = context.resource(mapping, 'repo')
689 repo = context.resource(mapping, 'repo')
688 ctx = context.resource(mapping, 'ctx')
690 ctx = context.resource(mapping, 'ctx')
689 ns = repo.names[namespace]
691 ns = repo.names[namespace]
690 names = ns.names(repo, ctx.node())
692 names = ns.names(repo, ctx.node())
691 return compatlist(context, mapping, ns.templatename, names,
693 return compatlist(context, mapping, ns.templatename, names,
692 plural=namespace)
694 plural=namespace)
693
695
694 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
696 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
695 def shownamespaces(context, mapping):
697 def shownamespaces(context, mapping):
696 """Dict of lists. Names attached to this changeset per
698 """Dict of lists. Names attached to this changeset per
697 namespace."""
699 namespace."""
698 repo = context.resource(mapping, 'repo')
700 repo = context.resource(mapping, 'repo')
699 ctx = context.resource(mapping, 'ctx')
701 ctx = context.resource(mapping, 'ctx')
700 templ = context.resource(mapping, 'templ')
702 templ = context.resource(mapping, 'templ')
701
703
702 namespaces = util.sortdict()
704 namespaces = util.sortdict()
703 def makensmapfn(ns):
705 def makensmapfn(ns):
704 # 'name' for iterating over namespaces, templatename for local reference
706 # 'name' for iterating over namespaces, templatename for local reference
705 return lambda v: {'name': v, ns.templatename: v}
707 return lambda v: {'name': v, ns.templatename: v}
706
708
707 for k, ns in repo.names.iteritems():
709 for k, ns in repo.names.iteritems():
708 names = ns.names(repo, ctx.node())
710 names = ns.names(repo, ctx.node())
709 f = _showlist('name', names, templ, mapping)
711 f = _showlist('name', names, templ, mapping)
710 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
712 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
711
713
712 f = _showlist('namespace', list(namespaces), templ, mapping)
714 f = _showlist('namespace', list(namespaces), templ, mapping)
713
715
714 def makemap(ns):
716 def makemap(ns):
715 return {
717 return {
716 'namespace': ns,
718 'namespace': ns,
717 'names': namespaces[ns],
719 'names': namespaces[ns],
718 'builtin': repo.names[ns].builtin,
720 'builtin': repo.names[ns].builtin,
719 'colorname': repo.names[ns].colorname,
721 'colorname': repo.names[ns].colorname,
720 }
722 }
721
723
722 return _hybrid(f, namespaces, makemap, pycompat.identity)
724 return _hybrid(f, namespaces, makemap, pycompat.identity)
723
725
724 @templatekeyword('node', requires={'ctx'})
726 @templatekeyword('node', requires={'ctx'})
725 def shownode(context, mapping):
727 def shownode(context, mapping):
726 """String. The changeset identification hash, as a 40 hexadecimal
728 """String. The changeset identification hash, as a 40 hexadecimal
727 digit string.
729 digit string.
728 """
730 """
729 ctx = context.resource(mapping, 'ctx')
731 ctx = context.resource(mapping, 'ctx')
730 return ctx.hex()
732 return ctx.hex()
731
733
732 @templatekeyword('obsolete', requires={'ctx'})
734 @templatekeyword('obsolete', requires={'ctx'})
733 def showobsolete(context, mapping):
735 def showobsolete(context, mapping):
734 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
736 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
735 ctx = context.resource(mapping, 'ctx')
737 ctx = context.resource(mapping, 'ctx')
736 if ctx.obsolete():
738 if ctx.obsolete():
737 return 'obsolete'
739 return 'obsolete'
738 return ''
740 return ''
739
741
740 @templatekeyword('peerurls', requires={'repo'})
742 @templatekeyword('peerurls', requires={'repo'})
741 def showpeerurls(context, mapping):
743 def showpeerurls(context, mapping):
742 """A dictionary of repository locations defined in the [paths] section
744 """A dictionary of repository locations defined in the [paths] section
743 of your configuration file."""
745 of your configuration file."""
744 repo = context.resource(mapping, 'repo')
746 repo = context.resource(mapping, 'repo')
745 # see commands.paths() for naming of dictionary keys
747 # see commands.paths() for naming of dictionary keys
746 paths = repo.ui.paths
748 paths = repo.ui.paths
747 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
749 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
748 def makemap(k):
750 def makemap(k):
749 p = paths[k]
751 p = paths[k]
750 d = {'name': k, 'url': p.rawloc}
752 d = {'name': k, 'url': p.rawloc}
751 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
753 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
752 return d
754 return d
753 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
755 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
754
756
755 @templatekeyword("predecessors", requires={'repo', 'ctx'})
757 @templatekeyword("predecessors", requires={'repo', 'ctx'})
756 def showpredecessors(context, mapping):
758 def showpredecessors(context, mapping):
757 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
759 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
758 repo = context.resource(mapping, 'repo')
760 repo = context.resource(mapping, 'repo')
759 ctx = context.resource(mapping, 'ctx')
761 ctx = context.resource(mapping, 'ctx')
760 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
762 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
761 predecessors = map(hex, predecessors)
763 predecessors = map(hex, predecessors)
762
764
763 return _hybrid(None, predecessors,
765 return _hybrid(None, predecessors,
764 lambda x: {'ctx': repo[x], 'revcache': {}},
766 lambda x: {'ctx': repo[x], 'revcache': {}},
765 lambda x: scmutil.formatchangeid(repo[x]))
767 lambda x: scmutil.formatchangeid(repo[x]))
766
768
767 @templatekeyword('reporoot', requires={'repo'})
769 @templatekeyword('reporoot', requires={'repo'})
768 def showreporoot(context, mapping):
770 def showreporoot(context, mapping):
769 """String. The root directory of the current repository."""
771 """String. The root directory of the current repository."""
770 repo = context.resource(mapping, 'repo')
772 repo = context.resource(mapping, 'repo')
771 return repo.root
773 return repo.root
772
774
773 @templatekeyword("successorssets", requires={'repo', 'ctx'})
775 @templatekeyword("successorssets", requires={'repo', 'ctx'})
774 def showsuccessorssets(context, mapping):
776 def showsuccessorssets(context, mapping):
775 """Returns a string of sets of successors for a changectx. Format used
777 """Returns a string of sets of successors for a changectx. Format used
776 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
778 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
777 while also diverged into ctx3. (EXPERIMENTAL)"""
779 while also diverged into ctx3. (EXPERIMENTAL)"""
778 repo = context.resource(mapping, 'repo')
780 repo = context.resource(mapping, 'repo')
779 ctx = context.resource(mapping, 'ctx')
781 ctx = context.resource(mapping, 'ctx')
780 if not ctx.obsolete():
782 if not ctx.obsolete():
781 return ''
783 return ''
782
784
783 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
785 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
784 ssets = [[hex(n) for n in ss] for ss in ssets]
786 ssets = [[hex(n) for n in ss] for ss in ssets]
785
787
786 data = []
788 data = []
787 for ss in ssets:
789 for ss in ssets:
788 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
790 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
789 lambda x: scmutil.formatchangeid(repo[x]))
791 lambda x: scmutil.formatchangeid(repo[x]))
790 data.append(h)
792 data.append(h)
791
793
792 # Format the successorssets
794 # Format the successorssets
793 def render(d):
795 def render(d):
794 t = []
796 t = []
795 for i in d.gen():
797 for i in d.gen():
796 t.append(i)
798 t.append(i)
797 return "".join(t)
799 return "".join(t)
798
800
799 def gen(data):
801 def gen(data):
800 yield "; ".join(render(d) for d in data)
802 yield "; ".join(render(d) for d in data)
801
803
802 return _hybrid(gen(data), data, lambda x: {'successorset': x},
804 return _hybrid(gen(data), data, lambda x: {'successorset': x},
803 pycompat.identity)
805 pycompat.identity)
804
806
805 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
807 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
806 def showsuccsandmarkers(context, mapping):
808 def showsuccsandmarkers(context, mapping):
807 """Returns a list of dict for each final successor of ctx. The dict
809 """Returns a list of dict for each final successor of ctx. The dict
808 contains successors node id in "successors" keys and the list of
810 contains successors node id in "successors" keys and the list of
809 obs-markers from ctx to the set of successors in "markers".
811 obs-markers from ctx to the set of successors in "markers".
810 (EXPERIMENTAL)
812 (EXPERIMENTAL)
811 """
813 """
812 repo = context.resource(mapping, 'repo')
814 repo = context.resource(mapping, 'repo')
813 ctx = context.resource(mapping, 'ctx')
815 ctx = context.resource(mapping, 'ctx')
814 templ = context.resource(mapping, 'templ')
816 templ = context.resource(mapping, 'templ')
815
817
816 values = obsutil.successorsandmarkers(repo, ctx)
818 values = obsutil.successorsandmarkers(repo, ctx)
817
819
818 if values is None:
820 if values is None:
819 values = []
821 values = []
820
822
821 # Format successors and markers to avoid exposing binary to templates
823 # Format successors and markers to avoid exposing binary to templates
822 data = []
824 data = []
823 for i in values:
825 for i in values:
824 # Format successors
826 # Format successors
825 successors = i['successors']
827 successors = i['successors']
826
828
827 successors = [hex(n) for n in successors]
829 successors = [hex(n) for n in successors]
828 successors = _hybrid(None, successors,
830 successors = _hybrid(None, successors,
829 lambda x: {'ctx': repo[x], 'revcache': {}},
831 lambda x: {'ctx': repo[x], 'revcache': {}},
830 lambda x: scmutil.formatchangeid(repo[x]))
832 lambda x: scmutil.formatchangeid(repo[x]))
831
833
832 # Format markers
834 # Format markers
833 finalmarkers = []
835 finalmarkers = []
834 for m in i['markers']:
836 for m in i['markers']:
835 hexprec = hex(m[0])
837 hexprec = hex(m[0])
836 hexsucs = tuple(hex(n) for n in m[1])
838 hexsucs = tuple(hex(n) for n in m[1])
837 hexparents = None
839 hexparents = None
838 if m[5] is not None:
840 if m[5] is not None:
839 hexparents = tuple(hex(n) for n in m[5])
841 hexparents = tuple(hex(n) for n in m[5])
840 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
842 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
841 finalmarkers.append(newmarker)
843 finalmarkers.append(newmarker)
842
844
843 data.append({'successors': successors, 'markers': finalmarkers})
845 data.append({'successors': successors, 'markers': finalmarkers})
844
846
845 f = _showlist('succsandmarkers', data, templ, mapping)
847 f = _showlist('succsandmarkers', data, templ, mapping)
846 return _hybrid(f, data, lambda x: x, pycompat.identity)
848 return _hybrid(f, data, lambda x: x, pycompat.identity)
847
849
848 @templatekeyword('p1rev', requires={'ctx'})
850 @templatekeyword('p1rev', requires={'ctx'})
849 def showp1rev(context, mapping):
851 def showp1rev(context, mapping):
850 """Integer. The repository-local revision number of the changeset's
852 """Integer. The repository-local revision number of the changeset's
851 first parent, or -1 if the changeset has no parents."""
853 first parent, or -1 if the changeset has no parents."""
852 ctx = context.resource(mapping, 'ctx')
854 ctx = context.resource(mapping, 'ctx')
853 return ctx.p1().rev()
855 return ctx.p1().rev()
854
856
855 @templatekeyword('p2rev', requires={'ctx'})
857 @templatekeyword('p2rev', requires={'ctx'})
856 def showp2rev(context, mapping):
858 def showp2rev(context, mapping):
857 """Integer. The repository-local revision number of the changeset's
859 """Integer. The repository-local revision number of the changeset's
858 second parent, or -1 if the changeset has no second parent."""
860 second parent, or -1 if the changeset has no second parent."""
859 ctx = context.resource(mapping, 'ctx')
861 ctx = context.resource(mapping, 'ctx')
860 return ctx.p2().rev()
862 return ctx.p2().rev()
861
863
862 @templatekeyword('p1node', requires={'ctx'})
864 @templatekeyword('p1node', requires={'ctx'})
863 def showp1node(context, mapping):
865 def showp1node(context, mapping):
864 """String. The identification hash of the changeset's first parent,
866 """String. The identification hash of the changeset's first parent,
865 as a 40 digit hexadecimal string. If the changeset has no parents, all
867 as a 40 digit hexadecimal string. If the changeset has no parents, all
866 digits are 0."""
868 digits are 0."""
867 ctx = context.resource(mapping, 'ctx')
869 ctx = context.resource(mapping, 'ctx')
868 return ctx.p1().hex()
870 return ctx.p1().hex()
869
871
870 @templatekeyword('p2node', requires={'ctx'})
872 @templatekeyword('p2node', requires={'ctx'})
871 def showp2node(context, mapping):
873 def showp2node(context, mapping):
872 """String. The identification hash of the changeset's second
874 """String. The identification hash of the changeset's second
873 parent, as a 40 digit hexadecimal string. If the changeset has no second
875 parent, as a 40 digit hexadecimal string. If the changeset has no second
874 parent, all digits are 0."""
876 parent, all digits are 0."""
875 ctx = context.resource(mapping, 'ctx')
877 ctx = context.resource(mapping, 'ctx')
876 return ctx.p2().hex()
878 return ctx.p2().hex()
877
879
878 @templatekeyword('parents')
880 @templatekeyword('parents', requires={'repo', 'ctx', 'templ'})
879 def showparents(**args):
881 def showparents(context, mapping):
880 """List of strings. The parents of the changeset in "rev:node"
882 """List of strings. The parents of the changeset in "rev:node"
881 format. If the changeset has only one "natural" parent (the predecessor
883 format. If the changeset has only one "natural" parent (the predecessor
882 revision) nothing is shown."""
884 revision) nothing is shown."""
883 args = pycompat.byteskwargs(args)
885 repo = context.resource(mapping, 'repo')
884 repo = args['repo']
886 ctx = context.resource(mapping, 'ctx')
885 ctx = args['ctx']
887 templ = context.resource(mapping, 'templ')
886 pctxs = scmutil.meaningfulparents(repo, ctx)
888 pctxs = scmutil.meaningfulparents(repo, ctx)
887 prevs = [p.rev() for p in pctxs]
889 prevs = [p.rev() for p in pctxs]
888 parents = [[('rev', p.rev()),
890 parents = [[('rev', p.rev()),
889 ('node', p.hex()),
891 ('node', p.hex()),
890 ('phase', p.phasestr())]
892 ('phase', p.phasestr())]
891 for p in pctxs]
893 for p in pctxs]
892 f = _showlist('parent', parents, args['templ'], args)
894 f = _showlist('parent', parents, templ, mapping)
893 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
895 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
894 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
896 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
895
897
896 @templatekeyword('phase', requires={'ctx'})
898 @templatekeyword('phase', requires={'ctx'})
897 def showphase(context, mapping):
899 def showphase(context, mapping):
898 """String. The changeset phase name."""
900 """String. The changeset phase name."""
899 ctx = context.resource(mapping, 'ctx')
901 ctx = context.resource(mapping, 'ctx')
900 return ctx.phasestr()
902 return ctx.phasestr()
901
903
902 @templatekeyword('phaseidx', requires={'ctx'})
904 @templatekeyword('phaseidx', requires={'ctx'})
903 def showphaseidx(context, mapping):
905 def showphaseidx(context, mapping):
904 """Integer. The changeset phase index. (ADVANCED)"""
906 """Integer. The changeset phase index. (ADVANCED)"""
905 ctx = context.resource(mapping, 'ctx')
907 ctx = context.resource(mapping, 'ctx')
906 return ctx.phase()
908 return ctx.phase()
907
909
908 @templatekeyword('rev', requires={'ctx'})
910 @templatekeyword('rev', requires={'ctx'})
909 def showrev(context, mapping):
911 def showrev(context, mapping):
910 """Integer. The repository-local changeset revision number."""
912 """Integer. The repository-local changeset revision number."""
911 ctx = context.resource(mapping, 'ctx')
913 ctx = context.resource(mapping, 'ctx')
912 return scmutil.intrev(ctx)
914 return scmutil.intrev(ctx)
913
915
914 def showrevslist(context, mapping, name, revs):
916 def showrevslist(context, mapping, name, revs):
915 """helper to generate a list of revisions in which a mapped template will
917 """helper to generate a list of revisions in which a mapped template will
916 be evaluated"""
918 be evaluated"""
917 repo = context.resource(mapping, 'repo')
919 repo = context.resource(mapping, 'repo')
918 templ = context.resource(mapping, 'templ')
920 templ = context.resource(mapping, 'templ')
919 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
921 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
920 return _hybrid(f, revs,
922 return _hybrid(f, revs,
921 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
923 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
922 pycompat.identity, keytype=int)
924 pycompat.identity, keytype=int)
923
925
924 @templatekeyword('subrepos', requires={'ctx', 'templ'})
926 @templatekeyword('subrepos', requires={'ctx', 'templ'})
925 def showsubrepos(context, mapping):
927 def showsubrepos(context, mapping):
926 """List of strings. Updated subrepositories in the changeset."""
928 """List of strings. Updated subrepositories in the changeset."""
927 ctx = context.resource(mapping, 'ctx')
929 ctx = context.resource(mapping, 'ctx')
928 substate = ctx.substate
930 substate = ctx.substate
929 if not substate:
931 if not substate:
930 return compatlist(context, mapping, 'subrepo', [])
932 return compatlist(context, mapping, 'subrepo', [])
931 psubstate = ctx.parents()[0].substate or {}
933 psubstate = ctx.parents()[0].substate or {}
932 subrepos = []
934 subrepos = []
933 for sub in substate:
935 for sub in substate:
934 if sub not in psubstate or substate[sub] != psubstate[sub]:
936 if sub not in psubstate or substate[sub] != psubstate[sub]:
935 subrepos.append(sub) # modified or newly added in ctx
937 subrepos.append(sub) # modified or newly added in ctx
936 for sub in psubstate:
938 for sub in psubstate:
937 if sub not in substate:
939 if sub not in substate:
938 subrepos.append(sub) # removed in ctx
940 subrepos.append(sub) # removed in ctx
939 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
941 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
940
942
941 # don't remove "showtags" definition, even though namespaces will put
943 # don't remove "showtags" definition, even though namespaces will put
942 # a helper function for "tags" keyword into "keywords" map automatically,
944 # a helper function for "tags" keyword into "keywords" map automatically,
943 # because online help text is built without namespaces initialization
945 # because online help text is built without namespaces initialization
944 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
946 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
945 def showtags(context, mapping):
947 def showtags(context, mapping):
946 """List of strings. Any tags associated with the changeset."""
948 """List of strings. Any tags associated with the changeset."""
947 return shownames(context, mapping, 'tags')
949 return shownames(context, mapping, 'tags')
948
950
949 @templatekeyword('termwidth', requires={'ui'})
951 @templatekeyword('termwidth', requires={'ui'})
950 def showtermwidth(context, mapping):
952 def showtermwidth(context, mapping):
951 """Integer. The width of the current terminal."""
953 """Integer. The width of the current terminal."""
952 ui = context.resource(mapping, 'ui')
954 ui = context.resource(mapping, 'ui')
953 return ui.termwidth()
955 return ui.termwidth()
954
956
955 @templatekeyword('instabilities', requires={'ctx', 'templ'})
957 @templatekeyword('instabilities', requires={'ctx', 'templ'})
956 def showinstabilities(context, mapping):
958 def showinstabilities(context, mapping):
957 """List of strings. Evolution instabilities affecting the changeset.
959 """List of strings. Evolution instabilities affecting the changeset.
958 (EXPERIMENTAL)
960 (EXPERIMENTAL)
959 """
961 """
960 ctx = context.resource(mapping, 'ctx')
962 ctx = context.resource(mapping, 'ctx')
961 return compatlist(context, mapping, 'instability', ctx.instabilities(),
963 return compatlist(context, mapping, 'instability', ctx.instabilities(),
962 plural='instabilities')
964 plural='instabilities')
963
965
964 @templatekeyword('verbosity', requires={'ui'})
966 @templatekeyword('verbosity', requires={'ui'})
965 def showverbosity(context, mapping):
967 def showverbosity(context, mapping):
966 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
968 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
967 or ''."""
969 or ''."""
968 ui = context.resource(mapping, 'ui')
970 ui = context.resource(mapping, 'ui')
969 # see logcmdutil.changesettemplater for priority of these flags
971 # see logcmdutil.changesettemplater for priority of these flags
970 if ui.debugflag:
972 if ui.debugflag:
971 return 'debug'
973 return 'debug'
972 elif ui.quiet:
974 elif ui.quiet:
973 return 'quiet'
975 return 'quiet'
974 elif ui.verbose:
976 elif ui.verbose:
975 return 'verbose'
977 return 'verbose'
976 return ''
978 return ''
977
979
978 def loadkeyword(ui, extname, registrarobj):
980 def loadkeyword(ui, extname, registrarobj):
979 """Load template keyword from specified registrarobj
981 """Load template keyword from specified registrarobj
980 """
982 """
981 for name, func in registrarobj._table.iteritems():
983 for name, func in registrarobj._table.iteritems():
982 keywords[name] = func
984 keywords[name] = func
983
985
984 # tell hggettext to extract docstrings from these functions:
986 # tell hggettext to extract docstrings from these functions:
985 i18nfunctions = keywords.values()
987 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now