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