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