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