##// END OF EJS Templates
exthelper: reintroduce the ability to register filesets...
Matt Harbison -
r41100:8f40e21c default
parent child Browse files
Show More
@@ -1,381 +1,380
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 assume the remote repository also handles blob storage
91 91 # for http(s) URLs. Otherwise, lfs will prompt to set this when it must
92 92 # use this value.
93 93 # (default: unset)
94 94 url = https://example.com/repo.git/info/lfs
95 95
96 96 # Which files to track in LFS. Path tests are "**.extname" for file
97 97 # extensions, and "path:under/some/directory" for path prefix. Both
98 98 # are relative to the repository root.
99 99 # File size can be tested with the "size()" fileset, and tests can be
100 100 # joined with fileset operators. (See "hg help filesets.operators".)
101 101 #
102 102 # Some examples:
103 103 # - all() # everything
104 104 # - none() # nothing
105 105 # - size(">20MB") # larger than 20MB
106 106 # - !**.txt # anything not a *.txt file
107 107 # - **.zip | **.tar.gz | **.7z # some types of compressed files
108 108 # - path:bin # files under "bin" in the project root
109 109 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
110 110 # | (path:bin & !path:/bin/README) | size(">1GB")
111 111 # (default: none())
112 112 #
113 113 # This is ignored if there is a tracked '.hglfs' file, and this setting
114 114 # will eventually be deprecated and removed.
115 115 track = size(">10M")
116 116
117 117 # how many times to retry before giving up on transferring an object
118 118 retry = 5
119 119
120 120 # the local directory to store lfs files for sharing across local clones.
121 121 # If not set, the cache is located in an OS specific cache location.
122 122 usercache = /path/to/global/cache
123 123 """
124 124
125 125 from __future__ import absolute_import
126 126
127 127 import sys
128 128
129 129 from mercurial.i18n import _
130 130
131 131 from mercurial import (
132 132 config,
133 133 error,
134 134 exchange,
135 135 extensions,
136 136 exthelper,
137 137 filelog,
138 138 filesetlang,
139 139 localrepo,
140 140 minifileset,
141 141 node,
142 142 pycompat,
143 registrar,
144 143 repository,
145 144 revlog,
146 145 scmutil,
147 146 templateutil,
148 147 util,
149 148 )
150 149
151 150 from . import (
152 151 blobstore,
153 152 wireprotolfsserver,
154 153 wrapper,
155 154 )
156 155
157 156 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
158 157 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
159 158 # be specifying the version(s) of Mercurial they are tested with, or
160 159 # leave the attribute unspecified.
161 160 testedwith = 'ships-with-hg-core'
162 161
163 162 eh = exthelper.exthelper()
164 163 eh.merge(wrapper.eh)
165 164 eh.merge(wireprotolfsserver.eh)
166 165
167 166 cmdtable = eh.cmdtable
168 167 configtable = eh.configtable
169 168 extsetup = eh.finalextsetup
170 169 uisetup = eh.finaluisetup
170 filesetpredicate = eh.filesetpredicate
171 171 reposetup = eh.finalreposetup
172 172 templatekeyword = eh.templatekeyword
173 173
174 174 eh.configitem('experimental', 'lfs.serve',
175 175 default=True,
176 176 )
177 177 eh.configitem('experimental', 'lfs.user-agent',
178 178 default=None,
179 179 )
180 180 eh.configitem('experimental', 'lfs.disableusercache',
181 181 default=False,
182 182 )
183 183 eh.configitem('experimental', 'lfs.worker-enable',
184 184 default=False,
185 185 )
186 186
187 187 eh.configitem('lfs', 'url',
188 188 default=None,
189 189 )
190 190 eh.configitem('lfs', 'usercache',
191 191 default=None,
192 192 )
193 193 # Deprecated
194 194 eh.configitem('lfs', 'threshold',
195 195 default=None,
196 196 )
197 197 eh.configitem('lfs', 'track',
198 198 default='none()',
199 199 )
200 200 eh.configitem('lfs', 'retry',
201 201 default=5,
202 202 )
203 filesetpredicate = registrar.filesetpredicate()
204 203
205 204 lfsprocessor = (
206 205 wrapper.readfromstore,
207 206 wrapper.writetostore,
208 207 wrapper.bypasscheckhash,
209 208 )
210 209
211 210 def featuresetup(ui, supported):
212 211 # don't die on seeing a repo with the lfs requirement
213 212 supported |= {'lfs'}
214 213
215 214 @eh.uisetup
216 215 def _uisetup(ui):
217 216 localrepo.featuresetupfuncs.add(featuresetup)
218 217
219 218 @eh.reposetup
220 219 def _reposetup(ui, repo):
221 220 # Nothing to do with a remote repo
222 221 if not repo.local():
223 222 return
224 223
225 224 repo.svfs.lfslocalblobstore = blobstore.local(repo)
226 225 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
227 226
228 227 class lfsrepo(repo.__class__):
229 228 @localrepo.unfilteredmethod
230 229 def commitctx(self, ctx, error=False):
231 230 repo.svfs.options['lfstrack'] = _trackedmatcher(self)
232 231 return super(lfsrepo, self).commitctx(ctx, error)
233 232
234 233 repo.__class__ = lfsrepo
235 234
236 235 if 'lfs' not in repo.requirements:
237 236 def checkrequireslfs(ui, repo, **kwargs):
238 237 if 'lfs' in repo.requirements:
239 238 return 0
240 239
241 240 last = kwargs.get(r'node_last')
242 241 _bin = node.bin
243 242 if last:
244 243 s = repo.set('%n:%n', _bin(kwargs[r'node']), _bin(last))
245 244 else:
246 245 s = repo.set('%n', _bin(kwargs[r'node']))
247 246 match = repo.narrowmatch()
248 247 for ctx in s:
249 248 # TODO: is there a way to just walk the files in the commit?
250 249 if any(ctx[f].islfs() for f in ctx.files()
251 250 if f in ctx and match(f)):
252 251 repo.requirements.add('lfs')
253 252 repo.features.add(repository.REPO_FEATURE_LFS)
254 253 repo._writerequirements()
255 254 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
256 255 break
257 256
258 257 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
259 258 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
260 259 else:
261 260 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
262 261
263 262 def _trackedmatcher(repo):
264 263 """Return a function (path, size) -> bool indicating whether or not to
265 264 track a given file with lfs."""
266 265 if not repo.wvfs.exists('.hglfs'):
267 266 # No '.hglfs' in wdir. Fallback to config for now.
268 267 trackspec = repo.ui.config('lfs', 'track')
269 268
270 269 # deprecated config: lfs.threshold
271 270 threshold = repo.ui.configbytes('lfs', 'threshold')
272 271 if threshold:
273 272 filesetlang.parse(trackspec) # make sure syntax errors are confined
274 273 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
275 274
276 275 return minifileset.compile(trackspec)
277 276
278 277 data = repo.wvfs.tryread('.hglfs')
279 278 if not data:
280 279 return lambda p, s: False
281 280
282 281 # Parse errors here will abort with a message that points to the .hglfs file
283 282 # and line number.
284 283 cfg = config.config()
285 284 cfg.parse('.hglfs', data)
286 285
287 286 try:
288 287 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
289 288 for pattern, rule in cfg.items('track')]
290 289 except error.ParseError as e:
291 290 # The original exception gives no indicator that the error is in the
292 291 # .hglfs file, so add that.
293 292
294 293 # TODO: See if the line number of the file can be made available.
295 294 raise error.Abort(_('parse error in .hglfs: %s') % e)
296 295
297 296 def _match(path, size):
298 297 for pat, rule in rules:
299 298 if pat(path, size):
300 299 return rule(path, size)
301 300
302 301 return False
303 302
304 303 return _match
305 304
306 305 # Called by remotefilelog
307 306 def wrapfilelog(filelog):
308 307 wrapfunction = extensions.wrapfunction
309 308
310 309 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
311 310 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
312 311 wrapfunction(filelog, 'size', wrapper.filelogsize)
313 312
314 313 @eh.wrapfunction(localrepo, 'resolverevlogstorevfsoptions')
315 314 def _resolverevlogstorevfsoptions(orig, ui, requirements, features):
316 315 opts = orig(ui, requirements, features)
317 316 for name, module in extensions.extensions(ui):
318 317 if module is sys.modules[__name__]:
319 318 if revlog.REVIDX_EXTSTORED in opts[b'flagprocessors']:
320 319 msg = (_(b"cannot register multiple processors on flag '%#x'.")
321 320 % revlog.REVIDX_EXTSTORED)
322 321 raise error.Abort(msg)
323 322
324 323 opts[b'flagprocessors'][revlog.REVIDX_EXTSTORED] = lfsprocessor
325 324 break
326 325
327 326 return opts
328 327
329 328 @eh.extsetup
330 329 def _extsetup(ui):
331 330 wrapfilelog(filelog.filelog)
332 331
333 332 scmutil.fileprefetchhooks.add('lfs', wrapper._prefetchfiles)
334 333
335 334 # Make bundle choose changegroup3 instead of changegroup2. This affects
336 335 # "hg bundle" command. Note: it does not cover all bundle formats like
337 336 # "packed1". Using "packed1" with lfs will likely cause trouble.
338 337 exchange._bundlespeccontentopts["v2"]["cg.version"] = "03"
339 338
340 @filesetpredicate('lfs()')
339 @eh.filesetpredicate('lfs()')
341 340 def lfsfileset(mctx, x):
342 341 """File that uses LFS storage."""
343 342 # i18n: "lfs" is a keyword
344 343 filesetlang.getargs(x, 0, 0, _("lfs takes no arguments"))
345 344 ctx = mctx.ctx
346 345 def lfsfilep(f):
347 346 return wrapper.pointerfromctx(ctx, f, removed=True) is not None
348 347 return mctx.predicate(lfsfilep, predrepr='<lfs>')
349 348
350 349 @eh.templatekeyword('lfs_files', requires={'ctx'})
351 350 def lfsfiles(context, mapping):
352 351 """List of strings. All files modified, added, or removed by this
353 352 changeset."""
354 353 ctx = context.resource(mapping, 'ctx')
355 354
356 355 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
357 356 files = sorted(pointers.keys())
358 357
359 358 def pointer(v):
360 359 # In the file spec, version is first and the other keys are sorted.
361 360 sortkeyfunc = lambda x: (x[0] != 'version', x)
362 361 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
363 362 return util.sortdict(items)
364 363
365 364 makemap = lambda v: {
366 365 'file': v,
367 366 'lfsoid': pointers[v].oid() if pointers[v] else None,
368 367 'lfspointer': templateutil.hybriddict(pointer(v)),
369 368 }
370 369
371 370 # TODO: make the separator ', '?
372 371 f = templateutil._showcompatlist(context, mapping, 'lfs_file', files)
373 372 return templateutil.hybrid(f, files, makemap, pycompat.identity)
374 373
375 374 @eh.command('debuglfsupload',
376 375 [('r', 'rev', [], _('upload large files introduced by REV'))])
377 376 def debuglfsupload(ui, repo, **opts):
378 377 """upload lfs blobs added by the working copy parent or given revisions"""
379 378 revs = opts.get(r'rev', [])
380 379 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
381 380 wrapper.uploadblobs(repo, pointers)
@@ -1,281 +1,283
1 1 # Copyright 2012 Logilab SA <contact@logilab.fr>
2 2 # Pierre-Yves David <pierre-yves.david@ens-lyon.org>
3 3 # Octobus <contact@octobus.net>
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 #####################################################################
9 9 ### Extension helper ###
10 10 #####################################################################
11 11
12 12 from __future__ import absolute_import
13 13
14 14 from . import (
15 15 commands,
16 16 error,
17 17 extensions,
18 18 registrar,
19 19 )
20 20
21 21 class exthelper(object):
22 22 """Helper for modular extension setup
23 23
24 24 A single helper should be instantiated for each extension. Helper
25 25 methods are then used as decorators for various purpose.
26 26
27 27 All decorators return the original function and may be chained.
28 28 """
29 29
30 30 def __init__(self):
31 31 self._uipopulatecallables = []
32 32 self._uicallables = []
33 33 self._extcallables = []
34 34 self._repocallables = []
35 35 self._commandwrappers = []
36 36 self._extcommandwrappers = []
37 37 self._functionwrappers = []
38 38 self._duckpunchers = []
39 39 self.cmdtable = {}
40 40 self.command = registrar.command(self.cmdtable)
41 41 self.configtable = {}
42 42 self.configitem = registrar.configitem(self.configtable)
43 self.filesetpredicate = registrar.filesetpredicate()
43 44 self.revsetpredicate = registrar.revsetpredicate()
44 45 self.templatekeyword = registrar.templatekeyword()
45 46
46 47 def merge(self, other):
47 48 self._uicallables.extend(other._uicallables)
48 49 self._uipopulatecallables.extend(other._uipopulatecallables)
49 50 self._extcallables.extend(other._extcallables)
50 51 self._repocallables.extend(other._repocallables)
52 self.filesetpredicate._table.update(other.filesetpredicate._table)
51 53 self.revsetpredicate._table.update(other.revsetpredicate._table)
52 54 self.templatekeyword._table.update(other.templatekeyword._table)
53 55 self._commandwrappers.extend(other._commandwrappers)
54 56 self._extcommandwrappers.extend(other._extcommandwrappers)
55 57 self._functionwrappers.extend(other._functionwrappers)
56 58 self._duckpunchers.extend(other._duckpunchers)
57 59 self.cmdtable.update(other.cmdtable)
58 60 for section, items in other.configtable.iteritems():
59 61 if section in self.configtable:
60 62 self.configtable[section].update(items)
61 63 else:
62 64 self.configtable[section] = items
63 65
64 66 def finaluisetup(self, ui):
65 67 """Method to be used as the extension uisetup
66 68
67 69 The following operations belong here:
68 70
69 71 - Changes to ui.__class__ . The ui object that will be used to run the
70 72 command has not yet been created. Changes made here will affect ui
71 73 objects created after this, and in particular the ui that will be
72 74 passed to runcommand
73 75 - Command wraps (extensions.wrapcommand)
74 76 - Changes that need to be visible to other extensions: because
75 77 initialization occurs in phases (all extensions run uisetup, then all
76 78 run extsetup), a change made here will be visible to other extensions
77 79 during extsetup
78 80 - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch
79 81 module members
80 82 - Setup of pre-* and post-* hooks
81 83 - pushkey setup
82 84 """
83 85 for cont, funcname, func in self._duckpunchers:
84 86 setattr(cont, funcname, func)
85 87 for command, wrapper, opts in self._commandwrappers:
86 88 entry = extensions.wrapcommand(commands.table, command, wrapper)
87 89 if opts:
88 90 for opt in opts:
89 91 entry[1].append(opt)
90 92 for cont, funcname, wrapper in self._functionwrappers:
91 93 extensions.wrapfunction(cont, funcname, wrapper)
92 94 for c in self._uicallables:
93 95 c(ui)
94 96
95 97 def finaluipopulate(self, ui):
96 98 """Method to be used as the extension uipopulate
97 99
98 100 This is called once per ui instance to:
99 101
100 102 - Set up additional ui members
101 103 - Update configuration by ``ui.setconfig()``
102 104 - Extend the class dynamically
103 105 """
104 106 for c in self._uipopulatecallables:
105 107 c(ui)
106 108
107 109 def finalextsetup(self, ui):
108 110 """Method to be used as a the extension extsetup
109 111
110 112 The following operations belong here:
111 113
112 114 - Changes depending on the status of other extensions. (if
113 115 extensions.find('mq'))
114 116 - Add a global option to all commands
115 117 """
116 118 knownexts = {}
117 119
118 120 for ext, command, wrapper, opts in self._extcommandwrappers:
119 121 if ext not in knownexts:
120 122 try:
121 123 e = extensions.find(ext)
122 124 except KeyError:
123 125 # Extension isn't enabled, so don't bother trying to wrap
124 126 # it.
125 127 continue
126 128 knownexts[ext] = e.cmdtable
127 129 entry = extensions.wrapcommand(knownexts[ext], command, wrapper)
128 130 if opts:
129 131 for opt in opts:
130 132 entry[1].append(opt)
131 133
132 134 for c in self._extcallables:
133 135 c(ui)
134 136
135 137 def finalreposetup(self, ui, repo):
136 138 """Method to be used as the extension reposetup
137 139
138 140 The following operations belong here:
139 141
140 142 - All hooks but pre-* and post-*
141 143 - Modify configuration variables
142 144 - Changes to repo.__class__, repo.dirstate.__class__
143 145 """
144 146 for c in self._repocallables:
145 147 c(ui, repo)
146 148
147 149 def uisetup(self, call):
148 150 """Decorated function will be executed during uisetup
149 151
150 152 example::
151 153
152 154 @eh.uisetup
153 155 def setupbabar(ui):
154 156 print 'this is uisetup!'
155 157 """
156 158 self._uicallables.append(call)
157 159 return call
158 160
159 161 def uipopulate(self, call):
160 162 """Decorated function will be executed during uipopulate
161 163
162 164 example::
163 165
164 166 @eh.uipopulate
165 167 def setupfoo(ui):
166 168 print 'this is uipopulate!'
167 169 """
168 170 self._uipopulatecallables.append(call)
169 171 return call
170 172
171 173 def extsetup(self, call):
172 174 """Decorated function will be executed during extsetup
173 175
174 176 example::
175 177
176 178 @eh.extsetup
177 179 def setupcelestine(ui):
178 180 print 'this is extsetup!'
179 181 """
180 182 self._extcallables.append(call)
181 183 return call
182 184
183 185 def reposetup(self, call):
184 186 """Decorated function will be executed during reposetup
185 187
186 188 example::
187 189
188 190 @eh.reposetup
189 191 def setupzephir(ui, repo):
190 192 print 'this is reposetup!'
191 193 """
192 194 self._repocallables.append(call)
193 195 return call
194 196
195 197 def wrapcommand(self, command, extension=None, opts=None):
196 198 """Decorated function is a command wrapper
197 199
198 200 The name of the command must be given as the decorator argument.
199 201 The wrapping is installed during `uisetup`.
200 202
201 203 If the second option `extension` argument is provided, the wrapping
202 204 will be applied in the extension commandtable. This argument must be a
203 205 string that will be searched using `extension.find` if not found and
204 206 Abort error is raised. If the wrapping applies to an extension, it is
205 207 installed during `extsetup`.
206 208
207 209 example::
208 210
209 211 @eh.wrapcommand('summary')
210 212 def wrapsummary(orig, ui, repo, *args, **kwargs):
211 213 ui.note('Barry!')
212 214 return orig(ui, repo, *args, **kwargs)
213 215
214 216 The `opts` argument allows specifying a list of tuples for additional
215 217 arguments for the command. See ``mercurial.fancyopts.fancyopts()`` for
216 218 the format of the tuple.
217 219
218 220 """
219 221 if opts is None:
220 222 opts = []
221 223 else:
222 224 for opt in opts:
223 225 if not isinstance(opt, tuple):
224 226 raise error.ProgrammingError('opts must be list of tuples')
225 227 if len(opt) not in (4, 5):
226 228 msg = 'each opt tuple must contain 4 or 5 values'
227 229 raise error.ProgrammingError(msg)
228 230
229 231 def dec(wrapper):
230 232 if extension is None:
231 233 self._commandwrappers.append((command, wrapper, opts))
232 234 else:
233 235 self._extcommandwrappers.append((extension, command, wrapper,
234 236 opts))
235 237 return wrapper
236 238 return dec
237 239
238 240 def wrapfunction(self, container, funcname):
239 241 """Decorated function is a function wrapper
240 242
241 243 This function takes two arguments, the container and the name of the
242 244 function to wrap. The wrapping is performed during `uisetup`.
243 245 (there is no extension support)
244 246
245 247 example::
246 248
247 249 @eh.function(discovery, 'checkheads')
248 250 def wrapfunction(orig, *args, **kwargs):
249 251 ui.note('His head smashed in and his heart cut out')
250 252 return orig(*args, **kwargs)
251 253 """
252 254 def dec(wrapper):
253 255 self._functionwrappers.append((container, funcname, wrapper))
254 256 return wrapper
255 257 return dec
256 258
257 259 def addattr(self, container, funcname):
258 260 """Decorated function is to be added to the container
259 261
260 262 This function takes two arguments, the container and the name of the
261 263 function to wrap. The wrapping is performed during `uisetup`.
262 264
263 265 Adding attributes to a container like this is discouraged, because the
264 266 container modification is visible even in repositories that do not
265 267 have the extension loaded. Therefore, care must be taken that the
266 268 function doesn't make assumptions that the extension was loaded for the
267 269 current repository. For `ui` and `repo` instances, a better option is
268 270 to subclass the instance in `uipopulate` and `reposetup` respectively.
269 271
270 272 https://www.mercurial-scm.org/wiki/WritingExtensions
271 273
272 274 example::
273 275
274 276 @eh.addattr(context.changectx, 'babar')
275 277 def babar(ctx):
276 278 return 'babar' in ctx.description
277 279 """
278 280 def dec(func):
279 281 self._duckpunchers.append((container, funcname, func))
280 282 return func
281 283 return dec
General Comments 0
You need to be logged in to leave comments. Login now