##// END OF EJS Templates
lfs: migrate to the fileprefetch callback mechanism
Matt Harbison -
r36155:a991fcc4 default
parent child Browse files
Show More
@@ -1,392 +1,393
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 prompt setting this when it must use this value.
90 # if unset, lfs will prompt setting this when it must use this value.
91 # (default: unset)
91 # (default: unset)
92 url = https://example.com/repo.git/info/lfs
92 url = https://example.com/repo.git/info/lfs
93
93
94 # Which files to track in LFS. Path tests are "**.extname" for file
94 # Which files to track in LFS. Path tests are "**.extname" for file
95 # extensions, and "path:under/some/directory" for path prefix. Both
95 # extensions, and "path:under/some/directory" for path prefix. Both
96 # are relative to the repository root.
96 # are relative to the repository root.
97 # File size can be tested with the "size()" fileset, and tests can be
97 # File size can be tested with the "size()" fileset, and tests can be
98 # joined with fileset operators. (See "hg help filesets.operators".)
98 # joined with fileset operators. (See "hg help filesets.operators".)
99 #
99 #
100 # Some examples:
100 # Some examples:
101 # - all() # everything
101 # - all() # everything
102 # - none() # nothing
102 # - none() # nothing
103 # - size(">20MB") # larger than 20MB
103 # - size(">20MB") # larger than 20MB
104 # - !**.txt # anything not a *.txt file
104 # - !**.txt # anything not a *.txt file
105 # - **.zip | **.tar.gz | **.7z # some types of compressed files
105 # - **.zip | **.tar.gz | **.7z # some types of compressed files
106 # - path:bin # files under "bin" in the project root
106 # - path:bin # files under "bin" in the project root
107 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
107 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
108 # | (path:bin & !path:/bin/README) | size(">1GB")
108 # | (path:bin & !path:/bin/README) | size(">1GB")
109 # (default: none())
109 # (default: none())
110 #
110 #
111 # This is ignored if there is a tracked '.hglfs' file, and this setting
111 # This is ignored if there is a tracked '.hglfs' file, and this setting
112 # will eventually be deprecated and removed.
112 # will eventually be deprecated and removed.
113 track = size(">10M")
113 track = size(">10M")
114
114
115 # how many times to retry before giving up on transferring an object
115 # how many times to retry before giving up on transferring an object
116 retry = 5
116 retry = 5
117
117
118 # the local directory to store lfs files for sharing across local clones.
118 # the local directory to store lfs files for sharing across local clones.
119 # If not set, the cache is located in an OS specific cache location.
119 # If not set, the cache is located in an OS specific cache location.
120 usercache = /path/to/global/cache
120 usercache = /path/to/global/cache
121 """
121 """
122
122
123 from __future__ import absolute_import
123 from __future__ import absolute_import
124
124
125 from mercurial.i18n import _
125 from mercurial.i18n import _
126
126
127 from mercurial import (
127 from mercurial import (
128 bundle2,
128 bundle2,
129 changegroup,
129 changegroup,
130 cmdutil,
130 cmdutil,
131 config,
131 config,
132 context,
132 context,
133 error,
133 error,
134 exchange,
134 exchange,
135 extensions,
135 extensions,
136 filelog,
136 filelog,
137 fileset,
137 fileset,
138 hg,
138 hg,
139 localrepo,
139 localrepo,
140 merge,
140 merge,
141 minifileset,
141 minifileset,
142 node,
142 node,
143 pycompat,
143 pycompat,
144 registrar,
144 registrar,
145 revlog,
145 revlog,
146 scmutil,
146 scmutil,
147 templatekw,
147 templatekw,
148 upgrade,
148 upgrade,
149 util,
149 util,
150 vfs as vfsmod,
150 vfs as vfsmod,
151 wireproto,
151 wireproto,
152 )
152 )
153
153
154 from . import (
154 from . import (
155 blobstore,
155 blobstore,
156 wrapper,
156 wrapper,
157 )
157 )
158
158
159 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
159 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
160 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
160 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
161 # be specifying the version(s) of Mercurial they are tested with, or
161 # be specifying the version(s) of Mercurial they are tested with, or
162 # leave the attribute unspecified.
162 # leave the attribute unspecified.
163 testedwith = 'ships-with-hg-core'
163 testedwith = 'ships-with-hg-core'
164
164
165 configtable = {}
165 configtable = {}
166 configitem = registrar.configitem(configtable)
166 configitem = registrar.configitem(configtable)
167
167
168 configitem('experimental', 'lfs.user-agent',
168 configitem('experimental', 'lfs.user-agent',
169 default=None,
169 default=None,
170 )
170 )
171 configitem('experimental', 'lfs.worker-enable',
171 configitem('experimental', 'lfs.worker-enable',
172 default=False,
172 default=False,
173 )
173 )
174
174
175 configitem('lfs', 'url',
175 configitem('lfs', 'url',
176 default=None,
176 default=None,
177 )
177 )
178 configitem('lfs', 'usercache',
178 configitem('lfs', 'usercache',
179 default=None,
179 default=None,
180 )
180 )
181 # Deprecated
181 # Deprecated
182 configitem('lfs', 'threshold',
182 configitem('lfs', 'threshold',
183 default=None,
183 default=None,
184 )
184 )
185 configitem('lfs', 'track',
185 configitem('lfs', 'track',
186 default='none()',
186 default='none()',
187 )
187 )
188 configitem('lfs', 'retry',
188 configitem('lfs', 'retry',
189 default=5,
189 default=5,
190 )
190 )
191
191
192 cmdtable = {}
192 cmdtable = {}
193 command = registrar.command(cmdtable)
193 command = registrar.command(cmdtable)
194
194
195 templatekeyword = registrar.templatekeyword()
195 templatekeyword = registrar.templatekeyword()
196 filesetpredicate = registrar.filesetpredicate()
196 filesetpredicate = registrar.filesetpredicate()
197
197
198 def featuresetup(ui, supported):
198 def featuresetup(ui, supported):
199 # don't die on seeing a repo with the lfs requirement
199 # don't die on seeing a repo with the lfs requirement
200 supported |= {'lfs'}
200 supported |= {'lfs'}
201
201
202 def uisetup(ui):
202 def uisetup(ui):
203 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
203 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
204
204
205 def reposetup(ui, repo):
205 def reposetup(ui, repo):
206 # Nothing to do with a remote repo
206 # Nothing to do with a remote repo
207 if not repo.local():
207 if not repo.local():
208 return
208 return
209
209
210 repo.svfs.lfslocalblobstore = blobstore.local(repo)
210 repo.svfs.lfslocalblobstore = blobstore.local(repo)
211 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
211 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
212
212
213 class lfsrepo(repo.__class__):
213 class lfsrepo(repo.__class__):
214 @localrepo.unfilteredmethod
214 @localrepo.unfilteredmethod
215 def commitctx(self, ctx, error=False):
215 def commitctx(self, ctx, error=False):
216 repo.svfs.options['lfstrack'] = _trackedmatcher(self)
216 repo.svfs.options['lfstrack'] = _trackedmatcher(self)
217 return super(lfsrepo, self).commitctx(ctx, error)
217 return super(lfsrepo, self).commitctx(ctx, error)
218
218
219 repo.__class__ = lfsrepo
219 repo.__class__ = lfsrepo
220
220
221 if 'lfs' not in repo.requirements:
221 if 'lfs' not in repo.requirements:
222 def checkrequireslfs(ui, repo, **kwargs):
222 def checkrequireslfs(ui, repo, **kwargs):
223 if 'lfs' not in repo.requirements:
223 if 'lfs' not in repo.requirements:
224 last = kwargs.get('node_last')
224 last = kwargs.get('node_last')
225 _bin = node.bin
225 _bin = node.bin
226 if last:
226 if last:
227 s = repo.set('%n:%n', _bin(kwargs['node']), _bin(last))
227 s = repo.set('%n:%n', _bin(kwargs['node']), _bin(last))
228 else:
228 else:
229 s = repo.set('%n', _bin(kwargs['node']))
229 s = repo.set('%n', _bin(kwargs['node']))
230 for ctx in s:
230 for ctx in s:
231 # TODO: is there a way to just walk the files in the commit?
231 # TODO: is there a way to just walk the files in the commit?
232 if any(ctx[f].islfs() for f in ctx.files() if f in ctx):
232 if any(ctx[f].islfs() for f in ctx.files() if f in ctx):
233 repo.requirements.add('lfs')
233 repo.requirements.add('lfs')
234 repo._writerequirements()
234 repo._writerequirements()
235 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
235 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
236 break
236 break
237
237
238 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
238 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
239 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
239 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
240 else:
240 else:
241 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
241 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
242
242
243 def _trackedmatcher(repo):
243 def _trackedmatcher(repo):
244 """Return a function (path, size) -> bool indicating whether or not to
244 """Return a function (path, size) -> bool indicating whether or not to
245 track a given file with lfs."""
245 track a given file with lfs."""
246 if not repo.wvfs.exists('.hglfs'):
246 if not repo.wvfs.exists('.hglfs'):
247 # No '.hglfs' in wdir. Fallback to config for now.
247 # No '.hglfs' in wdir. Fallback to config for now.
248 trackspec = repo.ui.config('lfs', 'track')
248 trackspec = repo.ui.config('lfs', 'track')
249
249
250 # deprecated config: lfs.threshold
250 # deprecated config: lfs.threshold
251 threshold = repo.ui.configbytes('lfs', 'threshold')
251 threshold = repo.ui.configbytes('lfs', 'threshold')
252 if threshold:
252 if threshold:
253 fileset.parse(trackspec) # make sure syntax errors are confined
253 fileset.parse(trackspec) # make sure syntax errors are confined
254 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
254 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
255
255
256 return minifileset.compile(trackspec)
256 return minifileset.compile(trackspec)
257
257
258 data = repo.wvfs.tryread('.hglfs')
258 data = repo.wvfs.tryread('.hglfs')
259 if not data:
259 if not data:
260 return lambda p, s: False
260 return lambda p, s: False
261
261
262 # Parse errors here will abort with a message that points to the .hglfs file
262 # Parse errors here will abort with a message that points to the .hglfs file
263 # and line number.
263 # and line number.
264 cfg = config.config()
264 cfg = config.config()
265 cfg.parse('.hglfs', data)
265 cfg.parse('.hglfs', data)
266
266
267 try:
267 try:
268 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
268 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
269 for pattern, rule in cfg.items('track')]
269 for pattern, rule in cfg.items('track')]
270 except error.ParseError as e:
270 except error.ParseError as e:
271 # The original exception gives no indicator that the error is in the
271 # The original exception gives no indicator that the error is in the
272 # .hglfs file, so add that.
272 # .hglfs file, so add that.
273
273
274 # TODO: See if the line number of the file can be made available.
274 # TODO: See if the line number of the file can be made available.
275 raise error.Abort(_('parse error in .hglfs: %s') % e)
275 raise error.Abort(_('parse error in .hglfs: %s') % e)
276
276
277 def _match(path, size):
277 def _match(path, size):
278 for pat, rule in rules:
278 for pat, rule in rules:
279 if pat(path, size):
279 if pat(path, size):
280 return rule(path, size)
280 return rule(path, size)
281
281
282 return False
282 return False
283
283
284 return _match
284 return _match
285
285
286 def wrapfilelog(filelog):
286 def wrapfilelog(filelog):
287 wrapfunction = extensions.wrapfunction
287 wrapfunction = extensions.wrapfunction
288
288
289 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
289 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
290 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
290 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
291 wrapfunction(filelog, 'size', wrapper.filelogsize)
291 wrapfunction(filelog, 'size', wrapper.filelogsize)
292
292
293 def extsetup(ui):
293 def extsetup(ui):
294 wrapfilelog(filelog.filelog)
294 wrapfilelog(filelog.filelog)
295
295
296 wrapfunction = extensions.wrapfunction
296 wrapfunction = extensions.wrapfunction
297
297
298 wrapfunction(cmdutil, '_updatecatformatter', wrapper._updatecatformatter)
298 wrapfunction(cmdutil, '_updatecatformatter', wrapper._updatecatformatter)
299 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
299 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
300
300
301 wrapfunction(upgrade, '_finishdatamigration',
301 wrapfunction(upgrade, '_finishdatamigration',
302 wrapper.upgradefinishdatamigration)
302 wrapper.upgradefinishdatamigration)
303
303
304 wrapfunction(upgrade, 'preservedrequirements',
304 wrapfunction(upgrade, 'preservedrequirements',
305 wrapper.upgraderequirements)
305 wrapper.upgraderequirements)
306
306
307 wrapfunction(upgrade, 'supporteddestrequirements',
307 wrapfunction(upgrade, 'supporteddestrequirements',
308 wrapper.upgraderequirements)
308 wrapper.upgraderequirements)
309
309
310 wrapfunction(changegroup,
310 wrapfunction(changegroup,
311 'supportedoutgoingversions',
311 'supportedoutgoingversions',
312 wrapper.supportedoutgoingversions)
312 wrapper.supportedoutgoingversions)
313 wrapfunction(changegroup,
313 wrapfunction(changegroup,
314 'allsupportedversions',
314 'allsupportedversions',
315 wrapper.allsupportedversions)
315 wrapper.allsupportedversions)
316
316
317 wrapfunction(exchange, 'push', wrapper.push)
317 wrapfunction(exchange, 'push', wrapper.push)
318 wrapfunction(wireproto, '_capabilities', wrapper._capabilities)
318 wrapfunction(wireproto, '_capabilities', wrapper._capabilities)
319
319
320 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
320 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
321 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
321 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
322 context.basefilectx.islfs = wrapper.filectxislfs
322 context.basefilectx.islfs = wrapper.filectxislfs
323
323
324 revlog.addflagprocessor(
324 revlog.addflagprocessor(
325 revlog.REVIDX_EXTSTORED,
325 revlog.REVIDX_EXTSTORED,
326 (
326 (
327 wrapper.readfromstore,
327 wrapper.readfromstore,
328 wrapper.writetostore,
328 wrapper.writetostore,
329 wrapper.bypasscheckhash,
329 wrapper.bypasscheckhash,
330 ),
330 ),
331 )
331 )
332
332
333 wrapfunction(hg, 'clone', wrapper.hgclone)
333 wrapfunction(hg, 'clone', wrapper.hgclone)
334 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
334 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
335
335
336 wrapfunction(merge, 'applyupdates', wrapper.mergemodapplyupdates)
336 wrapfunction(merge, 'applyupdates', wrapper.mergemodapplyupdates)
337 wrapfunction(cmdutil, '_prefetchfiles', wrapper.cmdutilprefetchfiles)
337
338 scmutil.fileprefetchhooks.add('lfs', wrapper._prefetchfiles)
338
339
339 # Make bundle choose changegroup3 instead of changegroup2. This affects
340 # Make bundle choose changegroup3 instead of changegroup2. This affects
340 # "hg bundle" command. Note: it does not cover all bundle formats like
341 # "hg bundle" command. Note: it does not cover all bundle formats like
341 # "packed1". Using "packed1" with lfs will likely cause trouble.
342 # "packed1". Using "packed1" with lfs will likely cause trouble.
342 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
343 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
343 for k in names:
344 for k in names:
344 exchange._bundlespeccgversions[k] = '03'
345 exchange._bundlespeccgversions[k] = '03'
345
346
346 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
347 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
347 # options and blob stores are passed from othervfs to the new readonlyvfs.
348 # options and blob stores are passed from othervfs to the new readonlyvfs.
348 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
349 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
349
350
350 # when writing a bundle via "hg bundle" command, upload related LFS blobs
351 # when writing a bundle via "hg bundle" command, upload related LFS blobs
351 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
352 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
352
353
353 @filesetpredicate('lfs()', callstatus=True)
354 @filesetpredicate('lfs()', callstatus=True)
354 def lfsfileset(mctx, x):
355 def lfsfileset(mctx, x):
355 """File that uses LFS storage."""
356 """File that uses LFS storage."""
356 # i18n: "lfs" is a keyword
357 # i18n: "lfs" is a keyword
357 fileset.getargs(x, 0, 0, _("lfs takes no arguments"))
358 fileset.getargs(x, 0, 0, _("lfs takes no arguments"))
358 return [f for f in mctx.subset
359 return [f for f in mctx.subset
359 if wrapper.pointerfromctx(mctx.ctx, f, removed=True) is not None]
360 if wrapper.pointerfromctx(mctx.ctx, f, removed=True) is not None]
360
361
361 @templatekeyword('lfs_files')
362 @templatekeyword('lfs_files')
362 def lfsfiles(repo, ctx, **args):
363 def lfsfiles(repo, ctx, **args):
363 """List of strings. All files modified, added, or removed by this
364 """List of strings. All files modified, added, or removed by this
364 changeset."""
365 changeset."""
365 args = pycompat.byteskwargs(args)
366 args = pycompat.byteskwargs(args)
366
367
367 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
368 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
368 files = sorted(pointers.keys())
369 files = sorted(pointers.keys())
369
370
370 def pointer(v):
371 def pointer(v):
371 # In the file spec, version is first and the other keys are sorted.
372 # In the file spec, version is first and the other keys are sorted.
372 sortkeyfunc = lambda x: (x[0] != 'version', x)
373 sortkeyfunc = lambda x: (x[0] != 'version', x)
373 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
374 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
374 return util.sortdict(items)
375 return util.sortdict(items)
375
376
376 makemap = lambda v: {
377 makemap = lambda v: {
377 'file': v,
378 'file': v,
378 'lfsoid': pointers[v].oid() if pointers[v] else None,
379 'lfsoid': pointers[v].oid() if pointers[v] else None,
379 'lfspointer': templatekw.hybriddict(pointer(v)),
380 'lfspointer': templatekw.hybriddict(pointer(v)),
380 }
381 }
381
382
382 # TODO: make the separator ', '?
383 # TODO: make the separator ', '?
383 f = templatekw._showlist('lfs_file', files, args)
384 f = templatekw._showlist('lfs_file', files, args)
384 return templatekw._hybrid(f, files, makemap, pycompat.identity)
385 return templatekw._hybrid(f, files, makemap, pycompat.identity)
385
386
386 @command('debuglfsupload',
387 @command('debuglfsupload',
387 [('r', 'rev', [], _('upload large files introduced by REV'))])
388 [('r', 'rev', [], _('upload large files introduced by REV'))])
388 def debuglfsupload(ui, repo, **opts):
389 def debuglfsupload(ui, repo, **opts):
389 """upload lfs blobs added by the working copy parent or given revisions"""
390 """upload lfs blobs added by the working copy parent or given revisions"""
390 revs = opts.get('rev', [])
391 revs = opts.get('rev', [])
391 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
392 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
392 wrapper.uploadblobs(repo, pointers)
393 wrapper.uploadblobs(repo, pointers)
@@ -1,418 +1,412
1 # wrapper.py - methods wrapping core mercurial logic
1 # wrapper.py - methods wrapping core mercurial logic
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
10 import hashlib
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial.node import bin, nullid, short
13 from mercurial.node import bin, nullid, short
14
14
15 from mercurial import (
15 from mercurial import (
16 error,
16 error,
17 filelog,
17 filelog,
18 revlog,
18 revlog,
19 util,
19 util,
20 )
20 )
21
21
22 from ..largefiles import lfutil
22 from ..largefiles import lfutil
23
23
24 from . import (
24 from . import (
25 blobstore,
25 blobstore,
26 pointer,
26 pointer,
27 )
27 )
28
28
29 def supportedoutgoingversions(orig, repo):
29 def supportedoutgoingversions(orig, repo):
30 versions = orig(repo)
30 versions = orig(repo)
31 if 'lfs' in repo.requirements:
31 if 'lfs' in repo.requirements:
32 versions.discard('01')
32 versions.discard('01')
33 versions.discard('02')
33 versions.discard('02')
34 versions.add('03')
34 versions.add('03')
35 return versions
35 return versions
36
36
37 def allsupportedversions(orig, ui):
37 def allsupportedversions(orig, ui):
38 versions = orig(ui)
38 versions = orig(ui)
39 versions.add('03')
39 versions.add('03')
40 return versions
40 return versions
41
41
42 def _capabilities(orig, repo, proto):
42 def _capabilities(orig, repo, proto):
43 '''Wrap server command to announce lfs server capability'''
43 '''Wrap server command to announce lfs server capability'''
44 caps = orig(repo, proto)
44 caps = orig(repo, proto)
45 # XXX: change to 'lfs=serve' when separate git server isn't required?
45 # XXX: change to 'lfs=serve' when separate git server isn't required?
46 caps.append('lfs')
46 caps.append('lfs')
47 return caps
47 return caps
48
48
49 def bypasscheckhash(self, text):
49 def bypasscheckhash(self, text):
50 return False
50 return False
51
51
52 def readfromstore(self, text):
52 def readfromstore(self, text):
53 """Read filelog content from local blobstore transform for flagprocessor.
53 """Read filelog content from local blobstore transform for flagprocessor.
54
54
55 Default tranform for flagprocessor, returning contents from blobstore.
55 Default tranform for flagprocessor, returning contents from blobstore.
56 Returns a 2-typle (text, validatehash) where validatehash is True as the
56 Returns a 2-typle (text, validatehash) where validatehash is True as the
57 contents of the blobstore should be checked using checkhash.
57 contents of the blobstore should be checked using checkhash.
58 """
58 """
59 p = pointer.deserialize(text)
59 p = pointer.deserialize(text)
60 oid = p.oid()
60 oid = p.oid()
61 store = self.opener.lfslocalblobstore
61 store = self.opener.lfslocalblobstore
62 if not store.has(oid):
62 if not store.has(oid):
63 p.filename = self.filename
63 p.filename = self.filename
64 self.opener.lfsremoteblobstore.readbatch([p], store)
64 self.opener.lfsremoteblobstore.readbatch([p], store)
65
65
66 # The caller will validate the content
66 # The caller will validate the content
67 text = store.read(oid, verify=False)
67 text = store.read(oid, verify=False)
68
68
69 # pack hg filelog metadata
69 # pack hg filelog metadata
70 hgmeta = {}
70 hgmeta = {}
71 for k in p.keys():
71 for k in p.keys():
72 if k.startswith('x-hg-'):
72 if k.startswith('x-hg-'):
73 name = k[len('x-hg-'):]
73 name = k[len('x-hg-'):]
74 hgmeta[name] = p[k]
74 hgmeta[name] = p[k]
75 if hgmeta or text.startswith('\1\n'):
75 if hgmeta or text.startswith('\1\n'):
76 text = filelog.packmeta(hgmeta, text)
76 text = filelog.packmeta(hgmeta, text)
77
77
78 return (text, True)
78 return (text, True)
79
79
80 def writetostore(self, text):
80 def writetostore(self, text):
81 # hg filelog metadata (includes rename, etc)
81 # hg filelog metadata (includes rename, etc)
82 hgmeta, offset = filelog.parsemeta(text)
82 hgmeta, offset = filelog.parsemeta(text)
83 if offset and offset > 0:
83 if offset and offset > 0:
84 # lfs blob does not contain hg filelog metadata
84 # lfs blob does not contain hg filelog metadata
85 text = text[offset:]
85 text = text[offset:]
86
86
87 # git-lfs only supports sha256
87 # git-lfs only supports sha256
88 oid = hashlib.sha256(text).hexdigest()
88 oid = hashlib.sha256(text).hexdigest()
89 self.opener.lfslocalblobstore.write(oid, text)
89 self.opener.lfslocalblobstore.write(oid, text)
90
90
91 # replace contents with metadata
91 # replace contents with metadata
92 longoid = 'sha256:%s' % oid
92 longoid = 'sha256:%s' % oid
93 metadata = pointer.gitlfspointer(oid=longoid, size=str(len(text)))
93 metadata = pointer.gitlfspointer(oid=longoid, size=str(len(text)))
94
94
95 # by default, we expect the content to be binary. however, LFS could also
95 # by default, we expect the content to be binary. however, LFS could also
96 # be used for non-binary content. add a special entry for non-binary data.
96 # be used for non-binary content. add a special entry for non-binary data.
97 # this will be used by filectx.isbinary().
97 # this will be used by filectx.isbinary().
98 if not util.binary(text):
98 if not util.binary(text):
99 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
99 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
100 metadata['x-is-binary'] = '0'
100 metadata['x-is-binary'] = '0'
101
101
102 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
102 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
103 if hgmeta is not None:
103 if hgmeta is not None:
104 for k, v in hgmeta.iteritems():
104 for k, v in hgmeta.iteritems():
105 metadata['x-hg-%s' % k] = v
105 metadata['x-hg-%s' % k] = v
106
106
107 rawtext = metadata.serialize()
107 rawtext = metadata.serialize()
108 return (rawtext, False)
108 return (rawtext, False)
109
109
110 def _islfs(rlog, node=None, rev=None):
110 def _islfs(rlog, node=None, rev=None):
111 if rev is None:
111 if rev is None:
112 if node is None:
112 if node is None:
113 # both None - likely working copy content where node is not ready
113 # both None - likely working copy content where node is not ready
114 return False
114 return False
115 rev = rlog.rev(node)
115 rev = rlog.rev(node)
116 else:
116 else:
117 node = rlog.node(rev)
117 node = rlog.node(rev)
118 if node == nullid:
118 if node == nullid:
119 return False
119 return False
120 flags = rlog.flags(rev)
120 flags = rlog.flags(rev)
121 return bool(flags & revlog.REVIDX_EXTSTORED)
121 return bool(flags & revlog.REVIDX_EXTSTORED)
122
122
123 def filelogaddrevision(orig, self, text, transaction, link, p1, p2,
123 def filelogaddrevision(orig, self, text, transaction, link, p1, p2,
124 cachedelta=None, node=None,
124 cachedelta=None, node=None,
125 flags=revlog.REVIDX_DEFAULT_FLAGS, **kwds):
125 flags=revlog.REVIDX_DEFAULT_FLAGS, **kwds):
126 textlen = len(text)
126 textlen = len(text)
127 # exclude hg rename meta from file size
127 # exclude hg rename meta from file size
128 meta, offset = filelog.parsemeta(text)
128 meta, offset = filelog.parsemeta(text)
129 if offset:
129 if offset:
130 textlen -= offset
130 textlen -= offset
131
131
132 lfstrack = self.opener.options['lfstrack']
132 lfstrack = self.opener.options['lfstrack']
133
133
134 if lfstrack(self.filename, textlen):
134 if lfstrack(self.filename, textlen):
135 flags |= revlog.REVIDX_EXTSTORED
135 flags |= revlog.REVIDX_EXTSTORED
136
136
137 return orig(self, text, transaction, link, p1, p2, cachedelta=cachedelta,
137 return orig(self, text, transaction, link, p1, p2, cachedelta=cachedelta,
138 node=node, flags=flags, **kwds)
138 node=node, flags=flags, **kwds)
139
139
140 def filelogrenamed(orig, self, node):
140 def filelogrenamed(orig, self, node):
141 if _islfs(self, node):
141 if _islfs(self, node):
142 rawtext = self.revision(node, raw=True)
142 rawtext = self.revision(node, raw=True)
143 if not rawtext:
143 if not rawtext:
144 return False
144 return False
145 metadata = pointer.deserialize(rawtext)
145 metadata = pointer.deserialize(rawtext)
146 if 'x-hg-copy' in metadata and 'x-hg-copyrev' in metadata:
146 if 'x-hg-copy' in metadata and 'x-hg-copyrev' in metadata:
147 return metadata['x-hg-copy'], bin(metadata['x-hg-copyrev'])
147 return metadata['x-hg-copy'], bin(metadata['x-hg-copyrev'])
148 else:
148 else:
149 return False
149 return False
150 return orig(self, node)
150 return orig(self, node)
151
151
152 def filelogsize(orig, self, rev):
152 def filelogsize(orig, self, rev):
153 if _islfs(self, rev=rev):
153 if _islfs(self, rev=rev):
154 # fast path: use lfs metadata to answer size
154 # fast path: use lfs metadata to answer size
155 rawtext = self.revision(rev, raw=True)
155 rawtext = self.revision(rev, raw=True)
156 metadata = pointer.deserialize(rawtext)
156 metadata = pointer.deserialize(rawtext)
157 return int(metadata['size'])
157 return int(metadata['size'])
158 return orig(self, rev)
158 return orig(self, rev)
159
159
160 def filectxcmp(orig, self, fctx):
160 def filectxcmp(orig, self, fctx):
161 """returns True if text is different than fctx"""
161 """returns True if text is different than fctx"""
162 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
162 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
163 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
163 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
164 # fast path: check LFS oid
164 # fast path: check LFS oid
165 p1 = pointer.deserialize(self.rawdata())
165 p1 = pointer.deserialize(self.rawdata())
166 p2 = pointer.deserialize(fctx.rawdata())
166 p2 = pointer.deserialize(fctx.rawdata())
167 return p1.oid() != p2.oid()
167 return p1.oid() != p2.oid()
168 return orig(self, fctx)
168 return orig(self, fctx)
169
169
170 def filectxisbinary(orig, self):
170 def filectxisbinary(orig, self):
171 if self.islfs():
171 if self.islfs():
172 # fast path: use lfs metadata to answer isbinary
172 # fast path: use lfs metadata to answer isbinary
173 metadata = pointer.deserialize(self.rawdata())
173 metadata = pointer.deserialize(self.rawdata())
174 # if lfs metadata says nothing, assume it's binary by default
174 # if lfs metadata says nothing, assume it's binary by default
175 return bool(int(metadata.get('x-is-binary', 1)))
175 return bool(int(metadata.get('x-is-binary', 1)))
176 return orig(self)
176 return orig(self)
177
177
178 def filectxislfs(self):
178 def filectxislfs(self):
179 return _islfs(self.filelog(), self.filenode())
179 return _islfs(self.filelog(), self.filenode())
180
180
181 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
181 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
182 orig(fm, ctx, matcher, path, decode)
182 orig(fm, ctx, matcher, path, decode)
183 fm.data(rawdata=ctx[path].rawdata())
183 fm.data(rawdata=ctx[path].rawdata())
184
184
185 def convertsink(orig, sink):
185 def convertsink(orig, sink):
186 sink = orig(sink)
186 sink = orig(sink)
187 if sink.repotype == 'hg':
187 if sink.repotype == 'hg':
188 class lfssink(sink.__class__):
188 class lfssink(sink.__class__):
189 def putcommit(self, files, copies, parents, commit, source, revmap,
189 def putcommit(self, files, copies, parents, commit, source, revmap,
190 full, cleanp2):
190 full, cleanp2):
191 pc = super(lfssink, self).putcommit
191 pc = super(lfssink, self).putcommit
192 node = pc(files, copies, parents, commit, source, revmap, full,
192 node = pc(files, copies, parents, commit, source, revmap, full,
193 cleanp2)
193 cleanp2)
194
194
195 if 'lfs' not in self.repo.requirements:
195 if 'lfs' not in self.repo.requirements:
196 ctx = self.repo[node]
196 ctx = self.repo[node]
197
197
198 # The file list may contain removed files, so check for
198 # The file list may contain removed files, so check for
199 # membership before assuming it is in the context.
199 # membership before assuming it is in the context.
200 if any(f in ctx and ctx[f].islfs() for f, n in files):
200 if any(f in ctx and ctx[f].islfs() for f, n in files):
201 self.repo.requirements.add('lfs')
201 self.repo.requirements.add('lfs')
202 self.repo._writerequirements()
202 self.repo._writerequirements()
203
203
204 # Permanently enable lfs locally
204 # Permanently enable lfs locally
205 self.repo.vfs.append(
205 self.repo.vfs.append(
206 'hgrc', util.tonativeeol('\n[extensions]\nlfs=\n'))
206 'hgrc', util.tonativeeol('\n[extensions]\nlfs=\n'))
207
207
208 return node
208 return node
209
209
210 sink.__class__ = lfssink
210 sink.__class__ = lfssink
211
211
212 return sink
212 return sink
213
213
214 def vfsinit(orig, self, othervfs):
214 def vfsinit(orig, self, othervfs):
215 orig(self, othervfs)
215 orig(self, othervfs)
216 # copy lfs related options
216 # copy lfs related options
217 for k, v in othervfs.options.items():
217 for k, v in othervfs.options.items():
218 if k.startswith('lfs'):
218 if k.startswith('lfs'):
219 self.options[k] = v
219 self.options[k] = v
220 # also copy lfs blobstores. note: this can run before reposetup, so lfs
220 # also copy lfs blobstores. note: this can run before reposetup, so lfs
221 # blobstore attributes are not always ready at this time.
221 # blobstore attributes are not always ready at this time.
222 for name in ['lfslocalblobstore', 'lfsremoteblobstore']:
222 for name in ['lfslocalblobstore', 'lfsremoteblobstore']:
223 if util.safehasattr(othervfs, name):
223 if util.safehasattr(othervfs, name):
224 setattr(self, name, getattr(othervfs, name))
224 setattr(self, name, getattr(othervfs, name))
225
225
226 def hgclone(orig, ui, opts, *args, **kwargs):
226 def hgclone(orig, ui, opts, *args, **kwargs):
227 result = orig(ui, opts, *args, **kwargs)
227 result = orig(ui, opts, *args, **kwargs)
228
228
229 if result is not None:
229 if result is not None:
230 sourcerepo, destrepo = result
230 sourcerepo, destrepo = result
231 repo = destrepo.local()
231 repo = destrepo.local()
232
232
233 # When cloning to a remote repo (like through SSH), no repo is available
233 # When cloning to a remote repo (like through SSH), no repo is available
234 # from the peer. Therefore the hgrc can't be updated.
234 # from the peer. Therefore the hgrc can't be updated.
235 if not repo:
235 if not repo:
236 return result
236 return result
237
237
238 # If lfs is required for this repo, permanently enable it locally
238 # If lfs is required for this repo, permanently enable it locally
239 if 'lfs' in repo.requirements:
239 if 'lfs' in repo.requirements:
240 repo.vfs.append('hgrc',
240 repo.vfs.append('hgrc',
241 util.tonativeeol('\n[extensions]\nlfs=\n'))
241 util.tonativeeol('\n[extensions]\nlfs=\n'))
242
242
243 return result
243 return result
244
244
245 def hgpostshare(orig, sourcerepo, destrepo, bookmarks=True, defaultpath=None):
245 def hgpostshare(orig, sourcerepo, destrepo, bookmarks=True, defaultpath=None):
246 orig(sourcerepo, destrepo, bookmarks, defaultpath)
246 orig(sourcerepo, destrepo, bookmarks, defaultpath)
247
247
248 # If lfs is required for this repo, permanently enable it locally
248 # If lfs is required for this repo, permanently enable it locally
249 if 'lfs' in destrepo.requirements:
249 if 'lfs' in destrepo.requirements:
250 destrepo.vfs.append('hgrc', util.tonativeeol('\n[extensions]\nlfs=\n'))
250 destrepo.vfs.append('hgrc', util.tonativeeol('\n[extensions]\nlfs=\n'))
251
251
252 def _prefetchfiles(repo, ctx, files):
252 def _prefetchfiles(repo, ctx, files):
253 """Ensure that required LFS blobs are present, fetching them as a group if
253 """Ensure that required LFS blobs are present, fetching them as a group if
254 needed.
254 needed.
255
255
256 This is centralized logic for various prefetch hooks."""
256 This is centralized logic for various prefetch hooks."""
257 pointers = []
257 pointers = []
258 localstore = repo.svfs.lfslocalblobstore
258 localstore = repo.svfs.lfslocalblobstore
259
259
260 for f in files:
260 for f in files:
261 p = pointerfromctx(ctx, f)
261 p = pointerfromctx(ctx, f)
262 if p and not localstore.has(p.oid()):
262 if p and not localstore.has(p.oid()):
263 p.filename = f
263 p.filename = f
264 pointers.append(p)
264 pointers.append(p)
265
265
266 if pointers:
266 if pointers:
267 repo.svfs.lfsremoteblobstore.readbatch(pointers, localstore)
267 repo.svfs.lfsremoteblobstore.readbatch(pointers, localstore)
268
268
269 def cmdutilprefetchfiles(orig, repo, ctx, files):
270 """Prefetch the indicated files before they are accessed by a command."""
271 orig(repo, ctx, files)
272
273 _prefetchfiles(repo, ctx, files)
274
275 def mergemodapplyupdates(orig, repo, actions, wctx, mctx, overwrite,
269 def mergemodapplyupdates(orig, repo, actions, wctx, mctx, overwrite,
276 labels=None):
270 labels=None):
277 """Ensure that the required LFS blobs are present before applying updates,
271 """Ensure that the required LFS blobs are present before applying updates,
278 fetching them as a group if needed.
272 fetching them as a group if needed.
279
273
280 This has the effect of ensuring all necessary LFS blobs are present before
274 This has the effect of ensuring all necessary LFS blobs are present before
281 making working directory changes during an update (including after clone and
275 making working directory changes during an update (including after clone and
282 share) or merge."""
276 share) or merge."""
283
277
284 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
278 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
285 # don't touch mctx. 'cd' is skipped, because changed/deleted never resolves
279 # don't touch mctx. 'cd' is skipped, because changed/deleted never resolves
286 # to something from the remote side.
280 # to something from the remote side.
287 oplist = [actions[a] for a in 'g dc dg m'.split()]
281 oplist = [actions[a] for a in 'g dc dg m'.split()]
288
282
289 _prefetchfiles(repo, mctx,
283 _prefetchfiles(repo, mctx,
290 [f for sublist in oplist for f, args, msg in sublist])
284 [f for sublist in oplist for f, args, msg in sublist])
291
285
292 return orig(repo, actions, wctx, mctx, overwrite, labels)
286 return orig(repo, actions, wctx, mctx, overwrite, labels)
293
287
294 def _canskipupload(repo):
288 def _canskipupload(repo):
295 # if remotestore is a null store, upload is a no-op and can be skipped
289 # if remotestore is a null store, upload is a no-op and can be skipped
296 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
290 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
297
291
298 def candownload(repo):
292 def candownload(repo):
299 # if remotestore is a null store, downloads will lead to nothing
293 # if remotestore is a null store, downloads will lead to nothing
300 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
294 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
301
295
302 def uploadblobsfromrevs(repo, revs):
296 def uploadblobsfromrevs(repo, revs):
303 '''upload lfs blobs introduced by revs
297 '''upload lfs blobs introduced by revs
304
298
305 Note: also used by other extensions e. g. infinitepush. avoid renaming.
299 Note: also used by other extensions e. g. infinitepush. avoid renaming.
306 '''
300 '''
307 if _canskipupload(repo):
301 if _canskipupload(repo):
308 return
302 return
309 pointers = extractpointers(repo, revs)
303 pointers = extractpointers(repo, revs)
310 uploadblobs(repo, pointers)
304 uploadblobs(repo, pointers)
311
305
312 def prepush(pushop):
306 def prepush(pushop):
313 """Prepush hook.
307 """Prepush hook.
314
308
315 Read through the revisions to push, looking for filelog entries that can be
309 Read through the revisions to push, looking for filelog entries that can be
316 deserialized into metadata so that we can block the push on their upload to
310 deserialized into metadata so that we can block the push on their upload to
317 the remote blobstore.
311 the remote blobstore.
318 """
312 """
319 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
313 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
320
314
321 def push(orig, repo, remote, *args, **kwargs):
315 def push(orig, repo, remote, *args, **kwargs):
322 """bail on push if the extension isn't enabled on remote when needed"""
316 """bail on push if the extension isn't enabled on remote when needed"""
323 if 'lfs' in repo.requirements:
317 if 'lfs' in repo.requirements:
324 # If the remote peer is for a local repo, the requirement tests in the
318 # If the remote peer is for a local repo, the requirement tests in the
325 # base class method enforce lfs support. Otherwise, some revisions in
319 # base class method enforce lfs support. Otherwise, some revisions in
326 # this repo use lfs, and the remote repo needs the extension loaded.
320 # this repo use lfs, and the remote repo needs the extension loaded.
327 if not remote.local() and not remote.capable('lfs'):
321 if not remote.local() and not remote.capable('lfs'):
328 # This is a copy of the message in exchange.push() when requirements
322 # This is a copy of the message in exchange.push() when requirements
329 # are missing between local repos.
323 # are missing between local repos.
330 m = _("required features are not supported in the destination: %s")
324 m = _("required features are not supported in the destination: %s")
331 raise error.Abort(m % 'lfs',
325 raise error.Abort(m % 'lfs',
332 hint=_('enable the lfs extension on the server'))
326 hint=_('enable the lfs extension on the server'))
333 return orig(repo, remote, *args, **kwargs)
327 return orig(repo, remote, *args, **kwargs)
334
328
335 def writenewbundle(orig, ui, repo, source, filename, bundletype, outgoing,
329 def writenewbundle(orig, ui, repo, source, filename, bundletype, outgoing,
336 *args, **kwargs):
330 *args, **kwargs):
337 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
331 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
338 uploadblobsfromrevs(repo, outgoing.missing)
332 uploadblobsfromrevs(repo, outgoing.missing)
339 return orig(ui, repo, source, filename, bundletype, outgoing, *args,
333 return orig(ui, repo, source, filename, bundletype, outgoing, *args,
340 **kwargs)
334 **kwargs)
341
335
342 def extractpointers(repo, revs):
336 def extractpointers(repo, revs):
343 """return a list of lfs pointers added by given revs"""
337 """return a list of lfs pointers added by given revs"""
344 repo.ui.debug('lfs: computing set of blobs to upload\n')
338 repo.ui.debug('lfs: computing set of blobs to upload\n')
345 pointers = {}
339 pointers = {}
346 for r in revs:
340 for r in revs:
347 ctx = repo[r]
341 ctx = repo[r]
348 for p in pointersfromctx(ctx).values():
342 for p in pointersfromctx(ctx).values():
349 pointers[p.oid()] = p
343 pointers[p.oid()] = p
350 return sorted(pointers.values())
344 return sorted(pointers.values())
351
345
352 def pointerfromctx(ctx, f, removed=False):
346 def pointerfromctx(ctx, f, removed=False):
353 """return a pointer for the named file from the given changectx, or None if
347 """return a pointer for the named file from the given changectx, or None if
354 the file isn't LFS.
348 the file isn't LFS.
355
349
356 Optionally, the pointer for a file deleted from the context can be returned.
350 Optionally, the pointer for a file deleted from the context can be returned.
357 Since no such pointer is actually stored, and to distinguish from a non LFS
351 Since no such pointer is actually stored, and to distinguish from a non LFS
358 file, this pointer is represented by an empty dict.
352 file, this pointer is represented by an empty dict.
359 """
353 """
360 _ctx = ctx
354 _ctx = ctx
361 if f not in ctx:
355 if f not in ctx:
362 if not removed:
356 if not removed:
363 return None
357 return None
364 if f in ctx.p1():
358 if f in ctx.p1():
365 _ctx = ctx.p1()
359 _ctx = ctx.p1()
366 elif f in ctx.p2():
360 elif f in ctx.p2():
367 _ctx = ctx.p2()
361 _ctx = ctx.p2()
368 else:
362 else:
369 return None
363 return None
370 fctx = _ctx[f]
364 fctx = _ctx[f]
371 if not _islfs(fctx.filelog(), fctx.filenode()):
365 if not _islfs(fctx.filelog(), fctx.filenode()):
372 return None
366 return None
373 try:
367 try:
374 p = pointer.deserialize(fctx.rawdata())
368 p = pointer.deserialize(fctx.rawdata())
375 if ctx == _ctx:
369 if ctx == _ctx:
376 return p
370 return p
377 return {}
371 return {}
378 except pointer.InvalidPointer as ex:
372 except pointer.InvalidPointer as ex:
379 raise error.Abort(_('lfs: corrupted pointer (%s@%s): %s\n')
373 raise error.Abort(_('lfs: corrupted pointer (%s@%s): %s\n')
380 % (f, short(_ctx.node()), ex))
374 % (f, short(_ctx.node()), ex))
381
375
382 def pointersfromctx(ctx, removed=False):
376 def pointersfromctx(ctx, removed=False):
383 """return a dict {path: pointer} for given single changectx.
377 """return a dict {path: pointer} for given single changectx.
384
378
385 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
379 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
386 stored for the path is an empty dict.
380 stored for the path is an empty dict.
387 """
381 """
388 result = {}
382 result = {}
389 for f in ctx.files():
383 for f in ctx.files():
390 p = pointerfromctx(ctx, f, removed=removed)
384 p = pointerfromctx(ctx, f, removed=removed)
391 if p is not None:
385 if p is not None:
392 result[f] = p
386 result[f] = p
393 return result
387 return result
394
388
395 def uploadblobs(repo, pointers):
389 def uploadblobs(repo, pointers):
396 """upload given pointers from local blobstore"""
390 """upload given pointers from local blobstore"""
397 if not pointers:
391 if not pointers:
398 return
392 return
399
393
400 remoteblob = repo.svfs.lfsremoteblobstore
394 remoteblob = repo.svfs.lfsremoteblobstore
401 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
395 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
402
396
403 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
397 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
404 orig(ui, srcrepo, dstrepo, requirements)
398 orig(ui, srcrepo, dstrepo, requirements)
405
399
406 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
400 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
407 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
401 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
408
402
409 for dirpath, dirs, files in srclfsvfs.walk():
403 for dirpath, dirs, files in srclfsvfs.walk():
410 for oid in files:
404 for oid in files:
411 ui.write(_('copying lfs blob %s\n') % oid)
405 ui.write(_('copying lfs blob %s\n') % oid)
412 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
406 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
413
407
414 def upgraderequirements(orig, repo):
408 def upgraderequirements(orig, repo):
415 reqs = orig(repo)
409 reqs = orig(repo)
416 if 'lfs' in repo.requirements:
410 if 'lfs' in repo.requirements:
417 reqs.add('lfs')
411 reqs.add('lfs')
418 return reqs
412 return reqs
General Comments 0
You need to be logged in to leave comments. Login now