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