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