##// END OF EJS Templates
lfs: defer registering the pre-push hook until blobs are committed...
Matt Harbison -
r35753:693e3bca default
parent child Browse files
Show More
@@ -1,340 +1,340 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 The extension reads its configuration from a versioned ``.hglfs``
10 The extension reads its configuration from a versioned ``.hglfs``
11 configuration file found in the root of the working directory. The
11 configuration file found in the root of the working directory. The
12 ``.hglfs`` file uses the same syntax as all other Mercurial
12 ``.hglfs`` file uses the same syntax as all other Mercurial
13 configuration files. It uses a single section, ``[track]``.
13 configuration files. It uses a single section, ``[track]``.
14
14
15 The ``[track]`` section specifies which files are stored as LFS (or
15 The ``[track]`` section specifies which files are stored as LFS (or
16 not). Each line is keyed by a file pattern, with a predicate value.
16 not). Each line is keyed by a file pattern, with a predicate value.
17 The first file pattern match is used, so put more specific patterns
17 The first file pattern match is used, so put more specific patterns
18 first. The available predicates are ``all()``, ``none()``, and
18 first. The available predicates are ``all()``, ``none()``, and
19 ``size()``. See "hg help filesets.size" for the latter.
19 ``size()``. See "hg help filesets.size" for the latter.
20
20
21 Example versioned ``.hglfs`` file::
21 Example versioned ``.hglfs`` file::
22
22
23 [track]
23 [track]
24 # No Makefile or python file, anywhere, will be LFS
24 # No Makefile or python file, anywhere, will be LFS
25 **Makefile = none()
25 **Makefile = none()
26 **.py = none()
26 **.py = none()
27
27
28 **.zip = all()
28 **.zip = all()
29 **.exe = size(">1MB")
29 **.exe = size(">1MB")
30
30
31 # Catchall for everything not matched above
31 # Catchall for everything not matched above
32 ** = size(">10MB")
32 ** = size(">10MB")
33
33
34 Configs::
34 Configs::
35
35
36 [lfs]
36 [lfs]
37 # Remote endpoint. Multiple protocols are supported:
37 # Remote endpoint. Multiple protocols are supported:
38 # - http(s)://user:pass@example.com/path
38 # - http(s)://user:pass@example.com/path
39 # git-lfs endpoint
39 # git-lfs endpoint
40 # - file:///tmp/path
40 # - file:///tmp/path
41 # local filesystem, usually for testing
41 # local filesystem, usually for testing
42 # if unset, lfs will prompt setting this when it must use this value.
42 # if unset, lfs will prompt setting this when it must use this value.
43 # (default: unset)
43 # (default: unset)
44 url = https://example.com/lfs
44 url = https://example.com/lfs
45
45
46 # Which files to track in LFS. Path tests are "**.extname" for file
46 # Which files to track in LFS. Path tests are "**.extname" for file
47 # extensions, and "path:under/some/directory" for path prefix. Both
47 # extensions, and "path:under/some/directory" for path prefix. Both
48 # are relative to the repository root, and the latter must be quoted.
48 # are relative to the repository root, and the latter must be quoted.
49 # File size can be tested with the "size()" fileset, and tests can be
49 # File size can be tested with the "size()" fileset, and tests can be
50 # joined with fileset operators. (See "hg help filesets.operators".)
50 # joined with fileset operators. (See "hg help filesets.operators".)
51 #
51 #
52 # Some examples:
52 # Some examples:
53 # - all() # everything
53 # - all() # everything
54 # - none() # nothing
54 # - none() # nothing
55 # - size(">20MB") # larger than 20MB
55 # - size(">20MB") # larger than 20MB
56 # - !**.txt # anything not a *.txt file
56 # - !**.txt # anything not a *.txt file
57 # - **.zip | **.tar.gz | **.7z # some types of compressed files
57 # - **.zip | **.tar.gz | **.7z # some types of compressed files
58 # - "path:bin" # files under "bin" in the project root
58 # - "path:bin" # files under "bin" in the project root
59 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
59 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
60 # | ("path:bin" & !"path:/bin/README") | size(">1GB")
60 # | ("path:bin" & !"path:/bin/README") | size(">1GB")
61 # (default: none())
61 # (default: none())
62 #
62 #
63 # This is ignored if there is a tracked '.hglfs' file, and this setting
63 # This is ignored if there is a tracked '.hglfs' file, and this setting
64 # will eventually be deprecated and removed.
64 # will eventually be deprecated and removed.
65 track = size(">10M")
65 track = size(">10M")
66
66
67 # how many times to retry before giving up on transferring an object
67 # how many times to retry before giving up on transferring an object
68 retry = 5
68 retry = 5
69
69
70 # the local directory to store lfs files for sharing across local clones.
70 # the local directory to store lfs files for sharing across local clones.
71 # If not set, the cache is located in an OS specific cache location.
71 # If not set, the cache is located in an OS specific cache location.
72 usercache = /path/to/global/cache
72 usercache = /path/to/global/cache
73 """
73 """
74
74
75 from __future__ import absolute_import
75 from __future__ import absolute_import
76
76
77 from mercurial.i18n import _
77 from mercurial.i18n import _
78
78
79 from mercurial import (
79 from mercurial import (
80 bundle2,
80 bundle2,
81 changegroup,
81 changegroup,
82 cmdutil,
82 cmdutil,
83 config,
83 config,
84 context,
84 context,
85 error,
85 error,
86 exchange,
86 exchange,
87 extensions,
87 extensions,
88 filelog,
88 filelog,
89 fileset,
89 fileset,
90 hg,
90 hg,
91 localrepo,
91 localrepo,
92 minifileset,
92 minifileset,
93 node,
93 node,
94 pycompat,
94 pycompat,
95 registrar,
95 registrar,
96 revlog,
96 revlog,
97 scmutil,
97 scmutil,
98 templatekw,
98 templatekw,
99 upgrade,
99 upgrade,
100 util,
100 util,
101 vfs as vfsmod,
101 vfs as vfsmod,
102 wireproto,
102 wireproto,
103 )
103 )
104
104
105 from . import (
105 from . import (
106 blobstore,
106 blobstore,
107 wrapper,
107 wrapper,
108 )
108 )
109
109
110 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
110 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
111 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
111 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
112 # be specifying the version(s) of Mercurial they are tested with, or
112 # be specifying the version(s) of Mercurial they are tested with, or
113 # leave the attribute unspecified.
113 # leave the attribute unspecified.
114 testedwith = 'ships-with-hg-core'
114 testedwith = 'ships-with-hg-core'
115
115
116 configtable = {}
116 configtable = {}
117 configitem = registrar.configitem(configtable)
117 configitem = registrar.configitem(configtable)
118
118
119 configitem('experimental', 'lfs.user-agent',
119 configitem('experimental', 'lfs.user-agent',
120 default=None,
120 default=None,
121 )
121 )
122 configitem('experimental', 'lfs.worker-enable',
122 configitem('experimental', 'lfs.worker-enable',
123 default=False,
123 default=False,
124 )
124 )
125
125
126 configitem('lfs', 'url',
126 configitem('lfs', 'url',
127 default=None,
127 default=None,
128 )
128 )
129 configitem('lfs', 'usercache',
129 configitem('lfs', 'usercache',
130 default=None,
130 default=None,
131 )
131 )
132 # Deprecated
132 # Deprecated
133 configitem('lfs', 'threshold',
133 configitem('lfs', 'threshold',
134 default=None,
134 default=None,
135 )
135 )
136 configitem('lfs', 'track',
136 configitem('lfs', 'track',
137 default='none()',
137 default='none()',
138 )
138 )
139 configitem('lfs', 'retry',
139 configitem('lfs', 'retry',
140 default=5,
140 default=5,
141 )
141 )
142
142
143 cmdtable = {}
143 cmdtable = {}
144 command = registrar.command(cmdtable)
144 command = registrar.command(cmdtable)
145
145
146 templatekeyword = registrar.templatekeyword()
146 templatekeyword = registrar.templatekeyword()
147
147
148 def featuresetup(ui, supported):
148 def featuresetup(ui, supported):
149 # don't die on seeing a repo with the lfs requirement
149 # don't die on seeing a repo with the lfs requirement
150 supported |= {'lfs'}
150 supported |= {'lfs'}
151
151
152 def uisetup(ui):
152 def uisetup(ui):
153 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
153 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
154
154
155 def reposetup(ui, repo):
155 def reposetup(ui, repo):
156 # Nothing to do with a remote repo
156 # Nothing to do with a remote repo
157 if not repo.local():
157 if not repo.local():
158 return
158 return
159
159
160 repo.svfs.lfslocalblobstore = blobstore.local(repo)
160 repo.svfs.lfslocalblobstore = blobstore.local(repo)
161 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
161 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
162
162
163 # Push hook
164 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
165
166 class lfsrepo(repo.__class__):
163 class lfsrepo(repo.__class__):
167 @localrepo.unfilteredmethod
164 @localrepo.unfilteredmethod
168 def commitctx(self, ctx, error=False):
165 def commitctx(self, ctx, error=False):
169 repo.svfs.options['lfstrack'] = _trackedmatcher(self, ctx)
166 repo.svfs.options['lfstrack'] = _trackedmatcher(self, ctx)
170 return super(lfsrepo, self).commitctx(ctx, error)
167 return super(lfsrepo, self).commitctx(ctx, error)
171
168
172 repo.__class__ = lfsrepo
169 repo.__class__ = lfsrepo
173
170
174 if 'lfs' not in repo.requirements:
171 if 'lfs' not in repo.requirements:
175 def checkrequireslfs(ui, repo, **kwargs):
172 def checkrequireslfs(ui, repo, **kwargs):
176 if 'lfs' not in repo.requirements:
173 if 'lfs' not in repo.requirements:
177 last = kwargs.get('node_last')
174 last = kwargs.get('node_last')
178 _bin = node.bin
175 _bin = node.bin
179 if last:
176 if last:
180 s = repo.set('%n:%n', _bin(kwargs['node']), _bin(last))
177 s = repo.set('%n:%n', _bin(kwargs['node']), _bin(last))
181 else:
178 else:
182 s = repo.set('%n', _bin(kwargs['node']))
179 s = repo.set('%n', _bin(kwargs['node']))
183 for ctx in s:
180 for ctx in s:
184 # TODO: is there a way to just walk the files in the commit?
181 # TODO: is there a way to just walk the files in the commit?
185 if any(ctx[f].islfs() for f in ctx.files() if f in ctx):
182 if any(ctx[f].islfs() for f in ctx.files() if f in ctx):
186 repo.requirements.add('lfs')
183 repo.requirements.add('lfs')
187 repo._writerequirements()
184 repo._writerequirements()
185 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
188 break
186 break
189
187
190 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
188 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
191 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
189 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
190 else:
191 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
192
192
193 def _trackedmatcher(repo, ctx):
193 def _trackedmatcher(repo, ctx):
194 """Return a function (path, size) -> bool indicating whether or not to
194 """Return a function (path, size) -> bool indicating whether or not to
195 track a given file with lfs."""
195 track a given file with lfs."""
196 data = ''
196 data = ''
197
197
198 if '.hglfs' in ctx.added() or '.hglfs' in ctx.modified():
198 if '.hglfs' in ctx.added() or '.hglfs' in ctx.modified():
199 data = ctx['.hglfs'].data()
199 data = ctx['.hglfs'].data()
200 elif '.hglfs' not in ctx.removed():
200 elif '.hglfs' not in ctx.removed():
201 p1 = repo['.']
201 p1 = repo['.']
202
202
203 if '.hglfs' not in p1:
203 if '.hglfs' not in p1:
204 # No '.hglfs' in wdir or in parent. Fallback to config
204 # No '.hglfs' in wdir or in parent. Fallback to config
205 # for now.
205 # for now.
206 trackspec = repo.ui.config('lfs', 'track')
206 trackspec = repo.ui.config('lfs', 'track')
207
207
208 # deprecated config: lfs.threshold
208 # deprecated config: lfs.threshold
209 threshold = repo.ui.configbytes('lfs', 'threshold')
209 threshold = repo.ui.configbytes('lfs', 'threshold')
210 if threshold:
210 if threshold:
211 fileset.parse(trackspec) # make sure syntax errors are confined
211 fileset.parse(trackspec) # make sure syntax errors are confined
212 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
212 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
213
213
214 return minifileset.compile(trackspec)
214 return minifileset.compile(trackspec)
215
215
216 data = p1['.hglfs'].data()
216 data = p1['.hglfs'].data()
217
217
218 # In removed, or not in parent
218 # In removed, or not in parent
219 if not data:
219 if not data:
220 return lambda p, s: False
220 return lambda p, s: False
221
221
222 # Parse errors here will abort with a message that points to the .hglfs file
222 # Parse errors here will abort with a message that points to the .hglfs file
223 # and line number.
223 # and line number.
224 cfg = config.config()
224 cfg = config.config()
225 cfg.parse('.hglfs', data)
225 cfg.parse('.hglfs', data)
226
226
227 try:
227 try:
228 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
228 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
229 for pattern, rule in cfg.items('track')]
229 for pattern, rule in cfg.items('track')]
230 except error.ParseError as e:
230 except error.ParseError as e:
231 # The original exception gives no indicator that the error is in the
231 # The original exception gives no indicator that the error is in the
232 # .hglfs file, so add that.
232 # .hglfs file, so add that.
233
233
234 # TODO: See if the line number of the file can be made available.
234 # TODO: See if the line number of the file can be made available.
235 raise error.Abort(_('parse error in .hglfs: %s') % e)
235 raise error.Abort(_('parse error in .hglfs: %s') % e)
236
236
237 def _match(path, size):
237 def _match(path, size):
238 for pat, rule in rules:
238 for pat, rule in rules:
239 if pat(path, size):
239 if pat(path, size):
240 return rule(path, size)
240 return rule(path, size)
241
241
242 return False
242 return False
243
243
244 return _match
244 return _match
245
245
246 def wrapfilelog(filelog):
246 def wrapfilelog(filelog):
247 wrapfunction = extensions.wrapfunction
247 wrapfunction = extensions.wrapfunction
248
248
249 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
249 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
250 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
250 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
251 wrapfunction(filelog, 'size', wrapper.filelogsize)
251 wrapfunction(filelog, 'size', wrapper.filelogsize)
252
252
253 def extsetup(ui):
253 def extsetup(ui):
254 wrapfilelog(filelog.filelog)
254 wrapfilelog(filelog.filelog)
255
255
256 wrapfunction = extensions.wrapfunction
256 wrapfunction = extensions.wrapfunction
257
257
258 wrapfunction(cmdutil, '_updatecatformatter', wrapper._updatecatformatter)
258 wrapfunction(cmdutil, '_updatecatformatter', wrapper._updatecatformatter)
259 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
259 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
260
260
261 wrapfunction(upgrade, '_finishdatamigration',
261 wrapfunction(upgrade, '_finishdatamigration',
262 wrapper.upgradefinishdatamigration)
262 wrapper.upgradefinishdatamigration)
263
263
264 wrapfunction(upgrade, 'preservedrequirements',
264 wrapfunction(upgrade, 'preservedrequirements',
265 wrapper.upgraderequirements)
265 wrapper.upgraderequirements)
266
266
267 wrapfunction(upgrade, 'supporteddestrequirements',
267 wrapfunction(upgrade, 'supporteddestrequirements',
268 wrapper.upgraderequirements)
268 wrapper.upgraderequirements)
269
269
270 wrapfunction(changegroup,
270 wrapfunction(changegroup,
271 'supportedoutgoingversions',
271 'supportedoutgoingversions',
272 wrapper.supportedoutgoingversions)
272 wrapper.supportedoutgoingversions)
273 wrapfunction(changegroup,
273 wrapfunction(changegroup,
274 'allsupportedversions',
274 'allsupportedversions',
275 wrapper.allsupportedversions)
275 wrapper.allsupportedversions)
276
276
277 wrapfunction(exchange, 'push', wrapper.push)
277 wrapfunction(exchange, 'push', wrapper.push)
278 wrapfunction(wireproto, '_capabilities', wrapper._capabilities)
278 wrapfunction(wireproto, '_capabilities', wrapper._capabilities)
279
279
280 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
280 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
281 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
281 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
282 context.basefilectx.islfs = wrapper.filectxislfs
282 context.basefilectx.islfs = wrapper.filectxislfs
283
283
284 revlog.addflagprocessor(
284 revlog.addflagprocessor(
285 revlog.REVIDX_EXTSTORED,
285 revlog.REVIDX_EXTSTORED,
286 (
286 (
287 wrapper.readfromstore,
287 wrapper.readfromstore,
288 wrapper.writetostore,
288 wrapper.writetostore,
289 wrapper.bypasscheckhash,
289 wrapper.bypasscheckhash,
290 ),
290 ),
291 )
291 )
292
292
293 wrapfunction(hg, 'clone', wrapper.hgclone)
293 wrapfunction(hg, 'clone', wrapper.hgclone)
294 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
294 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
295
295
296 # Make bundle choose changegroup3 instead of changegroup2. This affects
296 # Make bundle choose changegroup3 instead of changegroup2. This affects
297 # "hg bundle" command. Note: it does not cover all bundle formats like
297 # "hg bundle" command. Note: it does not cover all bundle formats like
298 # "packed1". Using "packed1" with lfs will likely cause trouble.
298 # "packed1". Using "packed1" with lfs will likely cause trouble.
299 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
299 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
300 for k in names:
300 for k in names:
301 exchange._bundlespeccgversions[k] = '03'
301 exchange._bundlespeccgversions[k] = '03'
302
302
303 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
303 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
304 # options and blob stores are passed from othervfs to the new readonlyvfs.
304 # options and blob stores are passed from othervfs to the new readonlyvfs.
305 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
305 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
306
306
307 # when writing a bundle via "hg bundle" command, upload related LFS blobs
307 # when writing a bundle via "hg bundle" command, upload related LFS blobs
308 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
308 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
309
309
310 @templatekeyword('lfs_files')
310 @templatekeyword('lfs_files')
311 def lfsfiles(repo, ctx, **args):
311 def lfsfiles(repo, ctx, **args):
312 """List of strings. LFS files added or modified by the changeset."""
312 """List of strings. LFS files added or modified by the changeset."""
313 args = pycompat.byteskwargs(args)
313 args = pycompat.byteskwargs(args)
314
314
315 pointers = wrapper.pointersfromctx(ctx) # {path: pointer}
315 pointers = wrapper.pointersfromctx(ctx) # {path: pointer}
316 files = sorted(pointers.keys())
316 files = sorted(pointers.keys())
317
317
318 def lfsattrs(v):
318 def lfsattrs(v):
319 # In the file spec, version is first and the other keys are sorted.
319 # In the file spec, version is first and the other keys are sorted.
320 sortkeyfunc = lambda x: (x[0] != 'version', x)
320 sortkeyfunc = lambda x: (x[0] != 'version', x)
321 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
321 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
322 return util.sortdict(items)
322 return util.sortdict(items)
323
323
324 makemap = lambda v: {
324 makemap = lambda v: {
325 'file': v,
325 'file': v,
326 'oid': pointers[v].oid(),
326 'oid': pointers[v].oid(),
327 'lfsattrs': templatekw.hybriddict(lfsattrs(v)),
327 'lfsattrs': templatekw.hybriddict(lfsattrs(v)),
328 }
328 }
329
329
330 # TODO: make the separator ', '?
330 # TODO: make the separator ', '?
331 f = templatekw._showlist('lfs_file', files, args)
331 f = templatekw._showlist('lfs_file', files, args)
332 return templatekw._hybrid(f, files, makemap, pycompat.identity)
332 return templatekw._hybrid(f, files, makemap, pycompat.identity)
333
333
334 @command('debuglfsupload',
334 @command('debuglfsupload',
335 [('r', 'rev', [], _('upload large files introduced by REV'))])
335 [('r', 'rev', [], _('upload large files introduced by REV'))])
336 def debuglfsupload(ui, repo, **opts):
336 def debuglfsupload(ui, repo, **opts):
337 """upload lfs blobs added by the working copy parent or given revisions"""
337 """upload lfs blobs added by the working copy parent or given revisions"""
338 revs = opts.get('rev', [])
338 revs = opts.get('rev', [])
339 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
339 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
340 wrapper.uploadblobs(repo, pointers)
340 wrapper.uploadblobs(repo, pointers)
General Comments 0
You need to be logged in to leave comments. Login now