##// END OF EJS Templates
wrapfunction: use sysstr instead of bytes as argument in "lfs"...
marmoute -
r51679:dde4b55a default
parent child Browse files
Show More
@@ -1,446 +1,446
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 The extension reads its configuration from a versioned ``.hglfs``
56 The extension reads its configuration from a versioned ``.hglfs``
57 configuration file found in the root of the working directory. The
57 configuration file found in the root of the working directory. The
58 ``.hglfs`` file uses the same syntax as all other Mercurial
58 ``.hglfs`` file uses the same syntax as all other Mercurial
59 configuration files. It uses a single section, ``[track]``.
59 configuration files. It uses a single section, ``[track]``.
60
60
61 The ``[track]`` section specifies which files are stored as LFS (or
61 The ``[track]`` section specifies which files are stored as LFS (or
62 not). Each line is keyed by a file pattern, with a predicate value.
62 not). Each line is keyed by a file pattern, with a predicate value.
63 The first file pattern match is used, so put more specific patterns
63 The first file pattern match is used, so put more specific patterns
64 first. The available predicates are ``all()``, ``none()``, and
64 first. The available predicates are ``all()``, ``none()``, and
65 ``size()``. See "hg help filesets.size" for the latter.
65 ``size()``. See "hg help filesets.size" for the latter.
66
66
67 Example versioned ``.hglfs`` file::
67 Example versioned ``.hglfs`` file::
68
68
69 [track]
69 [track]
70 # No Makefile or python file, anywhere, will be LFS
70 # No Makefile or python file, anywhere, will be LFS
71 **Makefile = none()
71 **Makefile = none()
72 **.py = none()
72 **.py = none()
73
73
74 **.zip = all()
74 **.zip = all()
75 **.exe = size(">1MB")
75 **.exe = size(">1MB")
76
76
77 # Catchall for everything not matched above
77 # Catchall for everything not matched above
78 ** = size(">10MB")
78 ** = size(">10MB")
79
79
80 Configs::
80 Configs::
81
81
82 [lfs]
82 [lfs]
83 # Remote endpoint. Multiple protocols are supported:
83 # Remote endpoint. Multiple protocols are supported:
84 # - http(s)://user:pass@example.com/path
84 # - http(s)://user:pass@example.com/path
85 # git-lfs endpoint
85 # git-lfs endpoint
86 # - file:///tmp/path
86 # - file:///tmp/path
87 # local filesystem, usually for testing
87 # local filesystem, usually for testing
88 # if unset, lfs will assume the remote repository also handles blob storage
88 # if unset, lfs will assume the remote repository also handles blob storage
89 # for http(s) URLs. Otherwise, lfs will prompt to set this when it must
89 # for http(s) URLs. Otherwise, lfs will prompt to set this when it must
90 # use this value.
90 # 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
123
124 import sys
124 import sys
125
125
126 from mercurial.i18n import _
126 from mercurial.i18n import _
127 from mercurial.node import bin
127 from mercurial.node import bin
128
128
129 from mercurial import (
129 from mercurial import (
130 bundlecaches,
130 bundlecaches,
131 config,
131 config,
132 context,
132 context,
133 error,
133 error,
134 extensions,
134 extensions,
135 exthelper,
135 exthelper,
136 filelog,
136 filelog,
137 filesetlang,
137 filesetlang,
138 localrepo,
138 localrepo,
139 logcmdutil,
139 logcmdutil,
140 minifileset,
140 minifileset,
141 pycompat,
141 pycompat,
142 revlog,
142 revlog,
143 scmutil,
143 scmutil,
144 templateutil,
144 templateutil,
145 util,
145 util,
146 )
146 )
147
147
148 from mercurial.interfaces import repository
148 from mercurial.interfaces import repository
149
149
150 from . import (
150 from . import (
151 blobstore,
151 blobstore,
152 wireprotolfsserver,
152 wireprotolfsserver,
153 wrapper,
153 wrapper,
154 )
154 )
155
155
156 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
156 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
157 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
157 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
158 # be specifying the version(s) of Mercurial they are tested with, or
158 # be specifying the version(s) of Mercurial they are tested with, or
159 # leave the attribute unspecified.
159 # leave the attribute unspecified.
160 testedwith = b'ships-with-hg-core'
160 testedwith = b'ships-with-hg-core'
161
161
162 eh = exthelper.exthelper()
162 eh = exthelper.exthelper()
163 eh.merge(wrapper.eh)
163 eh.merge(wrapper.eh)
164 eh.merge(wireprotolfsserver.eh)
164 eh.merge(wireprotolfsserver.eh)
165
165
166 cmdtable = eh.cmdtable
166 cmdtable = eh.cmdtable
167 configtable = eh.configtable
167 configtable = eh.configtable
168 extsetup = eh.finalextsetup
168 extsetup = eh.finalextsetup
169 uisetup = eh.finaluisetup
169 uisetup = eh.finaluisetup
170 filesetpredicate = eh.filesetpredicate
170 filesetpredicate = eh.filesetpredicate
171 reposetup = eh.finalreposetup
171 reposetup = eh.finalreposetup
172 templatekeyword = eh.templatekeyword
172 templatekeyword = eh.templatekeyword
173
173
174 eh.configitem(
174 eh.configitem(
175 b'experimental',
175 b'experimental',
176 b'lfs.serve',
176 b'lfs.serve',
177 default=True,
177 default=True,
178 )
178 )
179 eh.configitem(
179 eh.configitem(
180 b'experimental',
180 b'experimental',
181 b'lfs.user-agent',
181 b'lfs.user-agent',
182 default=None,
182 default=None,
183 )
183 )
184 eh.configitem(
184 eh.configitem(
185 b'experimental',
185 b'experimental',
186 b'lfs.disableusercache',
186 b'lfs.disableusercache',
187 default=False,
187 default=False,
188 )
188 )
189 eh.configitem(
189 eh.configitem(
190 b'experimental',
190 b'experimental',
191 b'lfs.worker-enable',
191 b'lfs.worker-enable',
192 default=True,
192 default=True,
193 )
193 )
194
194
195 eh.configitem(
195 eh.configitem(
196 b'lfs',
196 b'lfs',
197 b'url',
197 b'url',
198 default=None,
198 default=None,
199 )
199 )
200 eh.configitem(
200 eh.configitem(
201 b'lfs',
201 b'lfs',
202 b'usercache',
202 b'usercache',
203 default=None,
203 default=None,
204 )
204 )
205 # Deprecated
205 # Deprecated
206 eh.configitem(
206 eh.configitem(
207 b'lfs',
207 b'lfs',
208 b'threshold',
208 b'threshold',
209 default=None,
209 default=None,
210 )
210 )
211 eh.configitem(
211 eh.configitem(
212 b'lfs',
212 b'lfs',
213 b'track',
213 b'track',
214 default=b'none()',
214 default=b'none()',
215 )
215 )
216 eh.configitem(
216 eh.configitem(
217 b'lfs',
217 b'lfs',
218 b'retry',
218 b'retry',
219 default=5,
219 default=5,
220 )
220 )
221
221
222 lfsprocessor = (
222 lfsprocessor = (
223 wrapper.readfromstore,
223 wrapper.readfromstore,
224 wrapper.writetostore,
224 wrapper.writetostore,
225 wrapper.bypasscheckhash,
225 wrapper.bypasscheckhash,
226 )
226 )
227
227
228
228
229 def featuresetup(ui, supported):
229 def featuresetup(ui, supported):
230 # don't die on seeing a repo with the lfs requirement
230 # don't die on seeing a repo with the lfs requirement
231 supported |= {b'lfs'}
231 supported |= {b'lfs'}
232
232
233
233
234 @eh.uisetup
234 @eh.uisetup
235 def _uisetup(ui):
235 def _uisetup(ui):
236 localrepo.featuresetupfuncs.add(featuresetup)
236 localrepo.featuresetupfuncs.add(featuresetup)
237
237
238
238
239 @eh.reposetup
239 @eh.reposetup
240 def _reposetup(ui, repo):
240 def _reposetup(ui, repo):
241 # Nothing to do with a remote repo
241 # Nothing to do with a remote repo
242 if not repo.local():
242 if not repo.local():
243 return
243 return
244
244
245 repo.svfs.lfslocalblobstore = blobstore.local(repo)
245 repo.svfs.lfslocalblobstore = blobstore.local(repo)
246 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
246 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
247
247
248 class lfsrepo(repo.__class__):
248 class lfsrepo(repo.__class__):
249 @localrepo.unfilteredmethod
249 @localrepo.unfilteredmethod
250 def commitctx(self, ctx, error=False, origctx=None):
250 def commitctx(self, ctx, error=False, origctx=None):
251 repo.svfs.options[b'lfstrack'] = _trackedmatcher(self)
251 repo.svfs.options[b'lfstrack'] = _trackedmatcher(self)
252 return super(lfsrepo, self).commitctx(ctx, error, origctx=origctx)
252 return super(lfsrepo, self).commitctx(ctx, error, origctx=origctx)
253
253
254 repo.__class__ = lfsrepo
254 repo.__class__ = lfsrepo
255
255
256 if b'lfs' not in repo.requirements:
256 if b'lfs' not in repo.requirements:
257
257
258 def checkrequireslfs(ui, repo, **kwargs):
258 def checkrequireslfs(ui, repo, **kwargs):
259 with repo.lock():
259 with repo.lock():
260 if b'lfs' in repo.requirements:
260 if b'lfs' in repo.requirements:
261 return 0
261 return 0
262
262
263 last = kwargs.get('node_last')
263 last = kwargs.get('node_last')
264 if last:
264 if last:
265 s = repo.set(b'%n:%n', bin(kwargs['node']), bin(last))
265 s = repo.set(b'%n:%n', bin(kwargs['node']), bin(last))
266 else:
266 else:
267 s = repo.set(b'%n', bin(kwargs['node']))
267 s = repo.set(b'%n', bin(kwargs['node']))
268 match = repo._storenarrowmatch
268 match = repo._storenarrowmatch
269 for ctx in s:
269 for ctx in s:
270 # TODO: is there a way to just walk the files in the commit?
270 # TODO: is there a way to just walk the files in the commit?
271 if any(
271 if any(
272 ctx[f].islfs()
272 ctx[f].islfs()
273 for f in ctx.files()
273 for f in ctx.files()
274 if f in ctx and match(f)
274 if f in ctx and match(f)
275 ):
275 ):
276 repo.requirements.add(b'lfs')
276 repo.requirements.add(b'lfs')
277 repo.features.add(repository.REPO_FEATURE_LFS)
277 repo.features.add(repository.REPO_FEATURE_LFS)
278 scmutil.writereporequirements(repo)
278 scmutil.writereporequirements(repo)
279 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
279 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
280 break
280 break
281
281
282 ui.setconfig(b'hooks', b'commit.lfs', checkrequireslfs, b'lfs')
282 ui.setconfig(b'hooks', b'commit.lfs', checkrequireslfs, b'lfs')
283 ui.setconfig(
283 ui.setconfig(
284 b'hooks', b'pretxnchangegroup.lfs', checkrequireslfs, b'lfs'
284 b'hooks', b'pretxnchangegroup.lfs', checkrequireslfs, b'lfs'
285 )
285 )
286 else:
286 else:
287 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
287 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
288
288
289
289
290 def _trackedmatcher(repo):
290 def _trackedmatcher(repo):
291 """Return a function (path, size) -> bool indicating whether or not to
291 """Return a function (path, size) -> bool indicating whether or not to
292 track a given file with lfs."""
292 track a given file with lfs."""
293 if not repo.wvfs.exists(b'.hglfs'):
293 if not repo.wvfs.exists(b'.hglfs'):
294 # No '.hglfs' in wdir. Fallback to config for now.
294 # No '.hglfs' in wdir. Fallback to config for now.
295 trackspec = repo.ui.config(b'lfs', b'track')
295 trackspec = repo.ui.config(b'lfs', b'track')
296
296
297 # deprecated config: lfs.threshold
297 # deprecated config: lfs.threshold
298 threshold = repo.ui.configbytes(b'lfs', b'threshold')
298 threshold = repo.ui.configbytes(b'lfs', b'threshold')
299 if threshold:
299 if threshold:
300 filesetlang.parse(trackspec) # make sure syntax errors are confined
300 filesetlang.parse(trackspec) # make sure syntax errors are confined
301 trackspec = b"(%s) | size('>%d')" % (trackspec, threshold)
301 trackspec = b"(%s) | size('>%d')" % (trackspec, threshold)
302
302
303 return minifileset.compile(trackspec)
303 return minifileset.compile(trackspec)
304
304
305 data = repo.wvfs.tryread(b'.hglfs')
305 data = repo.wvfs.tryread(b'.hglfs')
306 if not data:
306 if not data:
307 return lambda p, s: False
307 return lambda p, s: False
308
308
309 # Parse errors here will abort with a message that points to the .hglfs file
309 # Parse errors here will abort with a message that points to the .hglfs file
310 # and line number.
310 # and line number.
311 cfg = config.config()
311 cfg = config.config()
312 cfg.parse(b'.hglfs', data)
312 cfg.parse(b'.hglfs', data)
313
313
314 try:
314 try:
315 rules = [
315 rules = [
316 (minifileset.compile(pattern), minifileset.compile(rule))
316 (minifileset.compile(pattern), minifileset.compile(rule))
317 for pattern, rule in cfg.items(b'track')
317 for pattern, rule in cfg.items(b'track')
318 ]
318 ]
319 except error.ParseError as e:
319 except error.ParseError as e:
320 # The original exception gives no indicator that the error is in the
320 # The original exception gives no indicator that the error is in the
321 # .hglfs file, so add that.
321 # .hglfs file, so add that.
322
322
323 # TODO: See if the line number of the file can be made available.
323 # TODO: See if the line number of the file can be made available.
324 raise error.Abort(_(b'parse error in .hglfs: %s') % e)
324 raise error.Abort(_(b'parse error in .hglfs: %s') % e)
325
325
326 def _match(path, size):
326 def _match(path, size):
327 for pat, rule in rules:
327 for pat, rule in rules:
328 if pat(path, size):
328 if pat(path, size):
329 return rule(path, size)
329 return rule(path, size)
330
330
331 return False
331 return False
332
332
333 return _match
333 return _match
334
334
335
335
336 # Called by remotefilelog
336 # Called by remotefilelog
337 def wrapfilelog(filelog):
337 def wrapfilelog(filelog):
338 wrapfunction = extensions.wrapfunction
338 wrapfunction = extensions.wrapfunction
339
339
340 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
340 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
341 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
341 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
342 wrapfunction(filelog, 'size', wrapper.filelogsize)
342 wrapfunction(filelog, 'size', wrapper.filelogsize)
343
343
344
344
345 @eh.wrapfunction(localrepo, b'resolverevlogstorevfsoptions')
345 @eh.wrapfunction(localrepo, 'resolverevlogstorevfsoptions')
346 def _resolverevlogstorevfsoptions(orig, ui, requirements, features):
346 def _resolverevlogstorevfsoptions(orig, ui, requirements, features):
347 opts = orig(ui, requirements, features)
347 opts = orig(ui, requirements, features)
348 for name, module in extensions.extensions(ui):
348 for name, module in extensions.extensions(ui):
349 if module is sys.modules[__name__]:
349 if module is sys.modules[__name__]:
350 if revlog.REVIDX_EXTSTORED in opts[b'flagprocessors']:
350 if revlog.REVIDX_EXTSTORED in opts[b'flagprocessors']:
351 msg = (
351 msg = (
352 _(b"cannot register multiple processors on flag '%#x'.")
352 _(b"cannot register multiple processors on flag '%#x'.")
353 % revlog.REVIDX_EXTSTORED
353 % revlog.REVIDX_EXTSTORED
354 )
354 )
355 raise error.Abort(msg)
355 raise error.Abort(msg)
356
356
357 opts[b'flagprocessors'][revlog.REVIDX_EXTSTORED] = lfsprocessor
357 opts[b'flagprocessors'][revlog.REVIDX_EXTSTORED] = lfsprocessor
358 break
358 break
359
359
360 return opts
360 return opts
361
361
362
362
363 @eh.extsetup
363 @eh.extsetup
364 def _extsetup(ui):
364 def _extsetup(ui):
365 wrapfilelog(filelog.filelog)
365 wrapfilelog(filelog.filelog)
366
366
367 context.basefilectx.islfs = wrapper.filectxislfs
367 context.basefilectx.islfs = wrapper.filectxislfs
368
368
369 scmutil.fileprefetchhooks.add(b'lfs', wrapper._prefetchfiles)
369 scmutil.fileprefetchhooks.add(b'lfs', wrapper._prefetchfiles)
370
370
371 # Make bundle choose changegroup3 instead of changegroup2. This affects
371 # Make bundle choose changegroup3 instead of changegroup2. This affects
372 # "hg bundle" command. Note: it does not cover all bundle formats like
372 # "hg bundle" command. Note: it does not cover all bundle formats like
373 # "packed1". Using "packed1" with lfs will likely cause trouble.
373 # "packed1". Using "packed1" with lfs will likely cause trouble.
374 bundlecaches._bundlespeccontentopts[b"v2"][b"cg.version"] = b"03"
374 bundlecaches._bundlespeccontentopts[b"v2"][b"cg.version"] = b"03"
375
375
376
376
377 @eh.filesetpredicate(b'lfs()')
377 @eh.filesetpredicate(b'lfs()')
378 def lfsfileset(mctx, x):
378 def lfsfileset(mctx, x):
379 """File that uses LFS storage."""
379 """File that uses LFS storage."""
380 # i18n: "lfs" is a keyword
380 # i18n: "lfs" is a keyword
381 filesetlang.getargs(x, 0, 0, _(b"lfs takes no arguments"))
381 filesetlang.getargs(x, 0, 0, _(b"lfs takes no arguments"))
382 ctx = mctx.ctx
382 ctx = mctx.ctx
383
383
384 def lfsfilep(f):
384 def lfsfilep(f):
385 return wrapper.pointerfromctx(ctx, f, removed=True) is not None
385 return wrapper.pointerfromctx(ctx, f, removed=True) is not None
386
386
387 return mctx.predicate(lfsfilep, predrepr=b'<lfs>')
387 return mctx.predicate(lfsfilep, predrepr=b'<lfs>')
388
388
389
389
390 @eh.templatekeyword(b'lfs_files', requires={b'ctx'})
390 @eh.templatekeyword(b'lfs_files', requires={b'ctx'})
391 def lfsfiles(context, mapping):
391 def lfsfiles(context, mapping):
392 """List of strings. All files modified, added, or removed by this
392 """List of strings. All files modified, added, or removed by this
393 changeset."""
393 changeset."""
394 ctx = context.resource(mapping, b'ctx')
394 ctx = context.resource(mapping, b'ctx')
395
395
396 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
396 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
397 files = sorted(pointers.keys())
397 files = sorted(pointers.keys())
398
398
399 def pointer(v):
399 def pointer(v):
400 # In the file spec, version is first and the other keys are sorted.
400 # In the file spec, version is first and the other keys are sorted.
401 sortkeyfunc = lambda x: (x[0] != b'version', x)
401 sortkeyfunc = lambda x: (x[0] != b'version', x)
402 items = sorted(pointers[v].items(), key=sortkeyfunc)
402 items = sorted(pointers[v].items(), key=sortkeyfunc)
403 return util.sortdict(items)
403 return util.sortdict(items)
404
404
405 makemap = lambda v: {
405 makemap = lambda v: {
406 b'file': v,
406 b'file': v,
407 b'lfsoid': pointers[v].oid() if pointers[v] else None,
407 b'lfsoid': pointers[v].oid() if pointers[v] else None,
408 b'lfspointer': templateutil.hybriddict(pointer(v)),
408 b'lfspointer': templateutil.hybriddict(pointer(v)),
409 }
409 }
410
410
411 # TODO: make the separator ', '?
411 # TODO: make the separator ', '?
412 f = templateutil._showcompatlist(context, mapping, b'lfs_file', files)
412 f = templateutil._showcompatlist(context, mapping, b'lfs_file', files)
413 return templateutil.hybrid(f, files, makemap, pycompat.identity)
413 return templateutil.hybrid(f, files, makemap, pycompat.identity)
414
414
415
415
416 @eh.command(
416 @eh.command(
417 b'debuglfsupload',
417 b'debuglfsupload',
418 [(b'r', b'rev', [], _(b'upload large files introduced by REV'))],
418 [(b'r', b'rev', [], _(b'upload large files introduced by REV'))],
419 )
419 )
420 def debuglfsupload(ui, repo, **opts):
420 def debuglfsupload(ui, repo, **opts):
421 """upload lfs blobs added by the working copy parent or given revisions"""
421 """upload lfs blobs added by the working copy parent or given revisions"""
422 revs = opts.get('rev', [])
422 revs = opts.get('rev', [])
423 pointers = wrapper.extractpointers(repo, logcmdutil.revrange(repo, revs))
423 pointers = wrapper.extractpointers(repo, logcmdutil.revrange(repo, revs))
424 wrapper.uploadblobs(repo, pointers)
424 wrapper.uploadblobs(repo, pointers)
425
425
426
426
427 @eh.wrapcommand(
427 @eh.wrapcommand(
428 b'verify',
428 b'verify',
429 opts=[(b'', b'no-lfs', None, _(b'skip missing lfs blob content'))],
429 opts=[(b'', b'no-lfs', None, _(b'skip missing lfs blob content'))],
430 )
430 )
431 def verify(orig, ui, repo, **opts):
431 def verify(orig, ui, repo, **opts):
432 skipflags = repo.ui.configint(b'verify', b'skipflags')
432 skipflags = repo.ui.configint(b'verify', b'skipflags')
433 no_lfs = opts.pop('no_lfs')
433 no_lfs = opts.pop('no_lfs')
434
434
435 if skipflags:
435 if skipflags:
436 # --lfs overrides the config bit, if set.
436 # --lfs overrides the config bit, if set.
437 if no_lfs is False:
437 if no_lfs is False:
438 skipflags &= ~repository.REVISION_FLAG_EXTSTORED
438 skipflags &= ~repository.REVISION_FLAG_EXTSTORED
439 else:
439 else:
440 skipflags = 0
440 skipflags = 0
441
441
442 if no_lfs is True:
442 if no_lfs is True:
443 skipflags |= repository.REVISION_FLAG_EXTSTORED
443 skipflags |= repository.REVISION_FLAG_EXTSTORED
444
444
445 with ui.configoverride({(b'verify', b'skipflags'): skipflags}):
445 with ui.configoverride({(b'verify', b'skipflags'): skipflags}):
446 return orig(ui, repo, **opts)
446 return orig(ui, repo, **opts)
@@ -1,369 +1,369
1 # wireprotolfsserver.py - lfs protocol server side implementation
1 # wireprotolfsserver.py - lfs protocol server side implementation
2 #
2 #
3 # Copyright 2018 Matt Harbison <matt_harbison@yahoo.com>
3 # Copyright 2018 Matt Harbison <matt_harbison@yahoo.com>
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 import datetime
9 import datetime
10 import errno
10 import errno
11 import json
11 import json
12 import traceback
12 import traceback
13
13
14 from mercurial.hgweb import common as hgwebcommon
14 from mercurial.hgweb import common as hgwebcommon
15
15
16 from mercurial import (
16 from mercurial import (
17 exthelper,
17 exthelper,
18 pycompat,
18 pycompat,
19 util,
19 util,
20 wireprotoserver,
20 wireprotoserver,
21 )
21 )
22
22
23 from . import blobstore
23 from . import blobstore
24
24
25 HTTP_OK = hgwebcommon.HTTP_OK
25 HTTP_OK = hgwebcommon.HTTP_OK
26 HTTP_CREATED = hgwebcommon.HTTP_CREATED
26 HTTP_CREATED = hgwebcommon.HTTP_CREATED
27 HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST
27 HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST
28 HTTP_NOT_FOUND = hgwebcommon.HTTP_NOT_FOUND
28 HTTP_NOT_FOUND = hgwebcommon.HTTP_NOT_FOUND
29 HTTP_METHOD_NOT_ALLOWED = hgwebcommon.HTTP_METHOD_NOT_ALLOWED
29 HTTP_METHOD_NOT_ALLOWED = hgwebcommon.HTTP_METHOD_NOT_ALLOWED
30 HTTP_NOT_ACCEPTABLE = hgwebcommon.HTTP_NOT_ACCEPTABLE
30 HTTP_NOT_ACCEPTABLE = hgwebcommon.HTTP_NOT_ACCEPTABLE
31 HTTP_UNSUPPORTED_MEDIA_TYPE = hgwebcommon.HTTP_UNSUPPORTED_MEDIA_TYPE
31 HTTP_UNSUPPORTED_MEDIA_TYPE = hgwebcommon.HTTP_UNSUPPORTED_MEDIA_TYPE
32
32
33 eh = exthelper.exthelper()
33 eh = exthelper.exthelper()
34
34
35
35
36 @eh.wrapfunction(wireprotoserver, b'handlewsgirequest')
36 @eh.wrapfunction(wireprotoserver, 'handlewsgirequest')
37 def handlewsgirequest(orig, rctx, req, res, checkperm):
37 def handlewsgirequest(orig, rctx, req, res, checkperm):
38 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
38 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
39 request if it is left unprocessed by the wrapped method.
39 request if it is left unprocessed by the wrapped method.
40 """
40 """
41 if orig(rctx, req, res, checkperm):
41 if orig(rctx, req, res, checkperm):
42 return True
42 return True
43
43
44 if not rctx.repo.ui.configbool(b'experimental', b'lfs.serve'):
44 if not rctx.repo.ui.configbool(b'experimental', b'lfs.serve'):
45 return False
45 return False
46
46
47 if not util.safehasattr(rctx.repo.svfs, 'lfslocalblobstore'):
47 if not util.safehasattr(rctx.repo.svfs, 'lfslocalblobstore'):
48 return False
48 return False
49
49
50 if not req.dispatchpath:
50 if not req.dispatchpath:
51 return False
51 return False
52
52
53 try:
53 try:
54 if req.dispatchpath == b'.git/info/lfs/objects/batch':
54 if req.dispatchpath == b'.git/info/lfs/objects/batch':
55 checkperm(rctx, req, b'pull')
55 checkperm(rctx, req, b'pull')
56 return _processbatchrequest(rctx.repo, req, res)
56 return _processbatchrequest(rctx.repo, req, res)
57 # TODO: reserve and use a path in the proposed http wireprotocol /api/
57 # TODO: reserve and use a path in the proposed http wireprotocol /api/
58 # namespace?
58 # namespace?
59 elif req.dispatchpath.startswith(b'.hg/lfs/objects'):
59 elif req.dispatchpath.startswith(b'.hg/lfs/objects'):
60 return _processbasictransfer(
60 return _processbasictransfer(
61 rctx.repo, req, res, lambda perm: checkperm(rctx, req, perm)
61 rctx.repo, req, res, lambda perm: checkperm(rctx, req, perm)
62 )
62 )
63 return False
63 return False
64 except hgwebcommon.ErrorResponse as e:
64 except hgwebcommon.ErrorResponse as e:
65 # XXX: copied from the handler surrounding wireprotoserver._callhttp()
65 # XXX: copied from the handler surrounding wireprotoserver._callhttp()
66 # in the wrapped function. Should this be moved back to hgweb to
66 # in the wrapped function. Should this be moved back to hgweb to
67 # be a common handler?
67 # be a common handler?
68 for k, v in e.headers:
68 for k, v in e.headers:
69 res.headers[k] = v
69 res.headers[k] = v
70 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
70 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
71 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
71 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
72 return True
72 return True
73
73
74
74
75 def _sethttperror(res, code, message=None):
75 def _sethttperror(res, code, message=None):
76 res.status = hgwebcommon.statusmessage(code, message=message)
76 res.status = hgwebcommon.statusmessage(code, message=message)
77 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
77 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
78 res.setbodybytes(b'')
78 res.setbodybytes(b'')
79
79
80
80
81 def _logexception(req):
81 def _logexception(req):
82 """Write information about the current exception to wsgi.errors."""
82 """Write information about the current exception to wsgi.errors."""
83 tb = pycompat.sysbytes(traceback.format_exc())
83 tb = pycompat.sysbytes(traceback.format_exc())
84 errorlog = req.rawenv[b'wsgi.errors']
84 errorlog = req.rawenv[b'wsgi.errors']
85
85
86 uri = b''
86 uri = b''
87 if req.apppath:
87 if req.apppath:
88 uri += req.apppath
88 uri += req.apppath
89 uri += b'/' + req.dispatchpath
89 uri += b'/' + req.dispatchpath
90
90
91 errorlog.write(
91 errorlog.write(
92 b"Exception happened while processing request '%s':\n%s" % (uri, tb)
92 b"Exception happened while processing request '%s':\n%s" % (uri, tb)
93 )
93 )
94
94
95
95
96 def _processbatchrequest(repo, req, res):
96 def _processbatchrequest(repo, req, res):
97 """Handle a request for the Batch API, which is the gateway to granting file
97 """Handle a request for the Batch API, which is the gateway to granting file
98 access.
98 access.
99
99
100 https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
100 https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
101 """
101 """
102
102
103 # Mercurial client request:
103 # Mercurial client request:
104 #
104 #
105 # HOST: localhost:$HGPORT
105 # HOST: localhost:$HGPORT
106 # ACCEPT: application/vnd.git-lfs+json
106 # ACCEPT: application/vnd.git-lfs+json
107 # ACCEPT-ENCODING: identity
107 # ACCEPT-ENCODING: identity
108 # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316)
108 # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316)
109 # Content-Length: 125
109 # Content-Length: 125
110 # Content-Type: application/vnd.git-lfs+json
110 # Content-Type: application/vnd.git-lfs+json
111 #
111 #
112 # {
112 # {
113 # "objects": [
113 # "objects": [
114 # {
114 # {
115 # "oid": "31cf...8e5b"
115 # "oid": "31cf...8e5b"
116 # "size": 12
116 # "size": 12
117 # }
117 # }
118 # ]
118 # ]
119 # "operation": "upload"
119 # "operation": "upload"
120 # }
120 # }
121
121
122 if req.method != b'POST':
122 if req.method != b'POST':
123 _sethttperror(res, HTTP_METHOD_NOT_ALLOWED)
123 _sethttperror(res, HTTP_METHOD_NOT_ALLOWED)
124 return True
124 return True
125
125
126 if req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json':
126 if req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json':
127 _sethttperror(res, HTTP_UNSUPPORTED_MEDIA_TYPE)
127 _sethttperror(res, HTTP_UNSUPPORTED_MEDIA_TYPE)
128 return True
128 return True
129
129
130 if req.headers[b'Accept'] != b'application/vnd.git-lfs+json':
130 if req.headers[b'Accept'] != b'application/vnd.git-lfs+json':
131 _sethttperror(res, HTTP_NOT_ACCEPTABLE)
131 _sethttperror(res, HTTP_NOT_ACCEPTABLE)
132 return True
132 return True
133
133
134 # XXX: specify an encoding?
134 # XXX: specify an encoding?
135 lfsreq = pycompat.json_loads(req.bodyfh.read())
135 lfsreq = pycompat.json_loads(req.bodyfh.read())
136
136
137 # If no transfer handlers are explicitly requested, 'basic' is assumed.
137 # If no transfer handlers are explicitly requested, 'basic' is assumed.
138 if 'basic' not in lfsreq.get('transfers', ['basic']):
138 if 'basic' not in lfsreq.get('transfers', ['basic']):
139 _sethttperror(
139 _sethttperror(
140 res,
140 res,
141 HTTP_BAD_REQUEST,
141 HTTP_BAD_REQUEST,
142 b'Only the basic LFS transfer handler is supported',
142 b'Only the basic LFS transfer handler is supported',
143 )
143 )
144 return True
144 return True
145
145
146 operation = lfsreq.get('operation')
146 operation = lfsreq.get('operation')
147 operation = pycompat.bytestr(operation)
147 operation = pycompat.bytestr(operation)
148
148
149 if operation not in (b'upload', b'download'):
149 if operation not in (b'upload', b'download'):
150 _sethttperror(
150 _sethttperror(
151 res,
151 res,
152 HTTP_BAD_REQUEST,
152 HTTP_BAD_REQUEST,
153 b'Unsupported LFS transfer operation: %s' % operation,
153 b'Unsupported LFS transfer operation: %s' % operation,
154 )
154 )
155 return True
155 return True
156
156
157 localstore = repo.svfs.lfslocalblobstore
157 localstore = repo.svfs.lfslocalblobstore
158
158
159 objects = [
159 objects = [
160 p
160 p
161 for p in _batchresponseobjects(
161 for p in _batchresponseobjects(
162 req, lfsreq.get('objects', []), operation, localstore
162 req, lfsreq.get('objects', []), operation, localstore
163 )
163 )
164 ]
164 ]
165
165
166 rsp = {
166 rsp = {
167 'transfer': 'basic',
167 'transfer': 'basic',
168 'objects': objects,
168 'objects': objects,
169 }
169 }
170
170
171 res.status = hgwebcommon.statusmessage(HTTP_OK)
171 res.status = hgwebcommon.statusmessage(HTTP_OK)
172 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json'
172 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json'
173 res.setbodybytes(pycompat.bytestr(json.dumps(rsp)))
173 res.setbodybytes(pycompat.bytestr(json.dumps(rsp)))
174
174
175 return True
175 return True
176
176
177
177
178 def _batchresponseobjects(req, objects, action, store):
178 def _batchresponseobjects(req, objects, action, store):
179 """Yield one dictionary of attributes for the Batch API response for each
179 """Yield one dictionary of attributes for the Batch API response for each
180 object in the list.
180 object in the list.
181
181
182 req: The parsedrequest for the Batch API request
182 req: The parsedrequest for the Batch API request
183 objects: The list of objects in the Batch API object request list
183 objects: The list of objects in the Batch API object request list
184 action: 'upload' or 'download'
184 action: 'upload' or 'download'
185 store: The local blob store for servicing requests"""
185 store: The local blob store for servicing requests"""
186
186
187 # Successful lfs-test-server response to solict an upload:
187 # Successful lfs-test-server response to solict an upload:
188 # {
188 # {
189 # u'objects': [{
189 # u'objects': [{
190 # u'size': 12,
190 # u'size': 12,
191 # u'oid': u'31cf...8e5b',
191 # u'oid': u'31cf...8e5b',
192 # u'actions': {
192 # u'actions': {
193 # u'upload': {
193 # u'upload': {
194 # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b',
194 # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b',
195 # u'expires_at': u'0001-01-01T00:00:00Z',
195 # u'expires_at': u'0001-01-01T00:00:00Z',
196 # u'header': {
196 # u'header': {
197 # u'Accept': u'application/vnd.git-lfs'
197 # u'Accept': u'application/vnd.git-lfs'
198 # }
198 # }
199 # }
199 # }
200 # }
200 # }
201 # }]
201 # }]
202 # }
202 # }
203
203
204 # TODO: Sort out the expires_at/expires_in/authenticated keys.
204 # TODO: Sort out the expires_at/expires_in/authenticated keys.
205
205
206 for obj in objects:
206 for obj in objects:
207 # Convert unicode to ASCII to create a filesystem path
207 # Convert unicode to ASCII to create a filesystem path
208 soid = obj.get('oid')
208 soid = obj.get('oid')
209 oid = soid.encode('ascii')
209 oid = soid.encode('ascii')
210 rsp = {
210 rsp = {
211 'oid': soid,
211 'oid': soid,
212 'size': obj.get('size'), # XXX: should this check the local size?
212 'size': obj.get('size'), # XXX: should this check the local size?
213 # 'authenticated': True,
213 # 'authenticated': True,
214 }
214 }
215
215
216 exists = True
216 exists = True
217 verifies = False
217 verifies = False
218
218
219 # Verify an existing file on the upload request, so that the client is
219 # Verify an existing file on the upload request, so that the client is
220 # solicited to re-upload if it corrupt locally. Download requests are
220 # solicited to re-upload if it corrupt locally. Download requests are
221 # also verified, so the error can be flagged in the Batch API response.
221 # also verified, so the error can be flagged in the Batch API response.
222 # (Maybe we can use this to short circuit the download for `hg verify`,
222 # (Maybe we can use this to short circuit the download for `hg verify`,
223 # IFF the client can assert that the remote end is an hg server.)
223 # IFF the client can assert that the remote end is an hg server.)
224 # Otherwise, it's potentially overkill on download, since it is also
224 # Otherwise, it's potentially overkill on download, since it is also
225 # verified as the file is streamed to the caller.
225 # verified as the file is streamed to the caller.
226 try:
226 try:
227 verifies = store.verify(oid)
227 verifies = store.verify(oid)
228 if verifies and action == b'upload':
228 if verifies and action == b'upload':
229 # The client will skip this upload, but make sure it remains
229 # The client will skip this upload, but make sure it remains
230 # available locally.
230 # available locally.
231 store.linkfromusercache(oid)
231 store.linkfromusercache(oid)
232 except IOError as inst:
232 except IOError as inst:
233 if inst.errno != errno.ENOENT:
233 if inst.errno != errno.ENOENT:
234 _logexception(req)
234 _logexception(req)
235
235
236 rsp['error'] = {
236 rsp['error'] = {
237 'code': 500,
237 'code': 500,
238 'message': inst.strerror or 'Internal Server Server',
238 'message': inst.strerror or 'Internal Server Server',
239 }
239 }
240 yield rsp
240 yield rsp
241 continue
241 continue
242
242
243 exists = False
243 exists = False
244
244
245 # Items are always listed for downloads. They are dropped for uploads
245 # Items are always listed for downloads. They are dropped for uploads
246 # IFF they already exist locally.
246 # IFF they already exist locally.
247 if action == b'download':
247 if action == b'download':
248 if not exists:
248 if not exists:
249 rsp['error'] = {
249 rsp['error'] = {
250 'code': 404,
250 'code': 404,
251 'message': "The object does not exist",
251 'message': "The object does not exist",
252 }
252 }
253 yield rsp
253 yield rsp
254 continue
254 continue
255
255
256 elif not verifies:
256 elif not verifies:
257 rsp['error'] = {
257 rsp['error'] = {
258 'code': 422, # XXX: is this the right code?
258 'code': 422, # XXX: is this the right code?
259 'message': "The object is corrupt",
259 'message': "The object is corrupt",
260 }
260 }
261 yield rsp
261 yield rsp
262 continue
262 continue
263
263
264 elif verifies:
264 elif verifies:
265 yield rsp # Skip 'actions': already uploaded
265 yield rsp # Skip 'actions': already uploaded
266 continue
266 continue
267
267
268 expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10)
268 expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10)
269
269
270 def _buildheader():
270 def _buildheader():
271 # The spec doesn't mention the Accept header here, but avoid
271 # The spec doesn't mention the Accept header here, but avoid
272 # a gratuitous deviation from lfs-test-server in the test
272 # a gratuitous deviation from lfs-test-server in the test
273 # output.
273 # output.
274 hdr = {'Accept': 'application/vnd.git-lfs'}
274 hdr = {'Accept': 'application/vnd.git-lfs'}
275
275
276 auth = req.headers.get(b'Authorization', b'')
276 auth = req.headers.get(b'Authorization', b'')
277 if auth.startswith(b'Basic '):
277 if auth.startswith(b'Basic '):
278 hdr['Authorization'] = pycompat.strurl(auth)
278 hdr['Authorization'] = pycompat.strurl(auth)
279
279
280 return hdr
280 return hdr
281
281
282 rsp['actions'] = {
282 rsp['actions'] = {
283 '%s'
283 '%s'
284 % pycompat.strurl(action): {
284 % pycompat.strurl(action): {
285 'href': pycompat.strurl(
285 'href': pycompat.strurl(
286 b'%s%s/.hg/lfs/objects/%s' % (req.baseurl, req.apppath, oid)
286 b'%s%s/.hg/lfs/objects/%s' % (req.baseurl, req.apppath, oid)
287 ),
287 ),
288 # datetime.isoformat() doesn't include the 'Z' suffix
288 # datetime.isoformat() doesn't include the 'Z' suffix
289 "expires_at": expiresat.strftime('%Y-%m-%dT%H:%M:%SZ'),
289 "expires_at": expiresat.strftime('%Y-%m-%dT%H:%M:%SZ'),
290 'header': _buildheader(),
290 'header': _buildheader(),
291 }
291 }
292 }
292 }
293
293
294 yield rsp
294 yield rsp
295
295
296
296
297 def _processbasictransfer(repo, req, res, checkperm):
297 def _processbasictransfer(repo, req, res, checkperm):
298 """Handle a single file upload (PUT) or download (GET) action for the Basic
298 """Handle a single file upload (PUT) or download (GET) action for the Basic
299 Transfer Adapter.
299 Transfer Adapter.
300
300
301 After determining if the request is for an upload or download, the access
301 After determining if the request is for an upload or download, the access
302 must be checked by calling ``checkperm()`` with either 'pull' or 'upload'
302 must be checked by calling ``checkperm()`` with either 'pull' or 'upload'
303 before accessing the files.
303 before accessing the files.
304
304
305 https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
305 https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
306 """
306 """
307
307
308 method = req.method
308 method = req.method
309 oid = req.dispatchparts[-1]
309 oid = req.dispatchparts[-1]
310 localstore = repo.svfs.lfslocalblobstore
310 localstore = repo.svfs.lfslocalblobstore
311
311
312 if len(req.dispatchparts) != 4:
312 if len(req.dispatchparts) != 4:
313 _sethttperror(res, HTTP_NOT_FOUND)
313 _sethttperror(res, HTTP_NOT_FOUND)
314 return True
314 return True
315
315
316 if method == b'PUT':
316 if method == b'PUT':
317 checkperm(b'upload')
317 checkperm(b'upload')
318
318
319 # TODO: verify Content-Type?
319 # TODO: verify Content-Type?
320
320
321 existed = localstore.has(oid)
321 existed = localstore.has(oid)
322
322
323 # TODO: how to handle timeouts? The body proxy handles limiting to
323 # TODO: how to handle timeouts? The body proxy handles limiting to
324 # Content-Length, but what happens if a client sends less than it
324 # Content-Length, but what happens if a client sends less than it
325 # says it will?
325 # says it will?
326
326
327 statusmessage = hgwebcommon.statusmessage
327 statusmessage = hgwebcommon.statusmessage
328 try:
328 try:
329 localstore.download(oid, req.bodyfh, req.headers[b'Content-Length'])
329 localstore.download(oid, req.bodyfh, req.headers[b'Content-Length'])
330 res.status = statusmessage(HTTP_OK if existed else HTTP_CREATED)
330 res.status = statusmessage(HTTP_OK if existed else HTTP_CREATED)
331 except blobstore.LfsCorruptionError:
331 except blobstore.LfsCorruptionError:
332 _logexception(req)
332 _logexception(req)
333
333
334 # XXX: Is this the right code?
334 # XXX: Is this the right code?
335 res.status = statusmessage(422, b'corrupt blob')
335 res.status = statusmessage(422, b'corrupt blob')
336
336
337 # There's no payload here, but this is the header that lfs-test-server
337 # There's no payload here, but this is the header that lfs-test-server
338 # sends back. This eliminates some gratuitous test output conditionals.
338 # sends back. This eliminates some gratuitous test output conditionals.
339 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
339 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
340 res.setbodybytes(b'')
340 res.setbodybytes(b'')
341
341
342 return True
342 return True
343 elif method == b'GET':
343 elif method == b'GET':
344 checkperm(b'pull')
344 checkperm(b'pull')
345
345
346 res.status = hgwebcommon.statusmessage(HTTP_OK)
346 res.status = hgwebcommon.statusmessage(HTTP_OK)
347 res.headers[b'Content-Type'] = b'application/octet-stream'
347 res.headers[b'Content-Type'] = b'application/octet-stream'
348
348
349 try:
349 try:
350 # TODO: figure out how to send back the file in chunks, instead of
350 # TODO: figure out how to send back the file in chunks, instead of
351 # reading the whole thing. (Also figure out how to send back
351 # reading the whole thing. (Also figure out how to send back
352 # an error status if an IOError occurs after a partial write
352 # an error status if an IOError occurs after a partial write
353 # in that case. Here, everything is read before starting.)
353 # in that case. Here, everything is read before starting.)
354 res.setbodybytes(localstore.read(oid))
354 res.setbodybytes(localstore.read(oid))
355 except blobstore.LfsCorruptionError:
355 except blobstore.LfsCorruptionError:
356 _logexception(req)
356 _logexception(req)
357
357
358 # XXX: Is this the right code?
358 # XXX: Is this the right code?
359 res.status = hgwebcommon.statusmessage(422, b'corrupt blob')
359 res.status = hgwebcommon.statusmessage(422, b'corrupt blob')
360 res.setbodybytes(b'')
360 res.setbodybytes(b'')
361
361
362 return True
362 return True
363 else:
363 else:
364 _sethttperror(
364 _sethttperror(
365 res,
365 res,
366 HTTP_METHOD_NOT_ALLOWED,
366 HTTP_METHOD_NOT_ALLOWED,
367 message=b'Unsupported LFS transfer method: %s' % method,
367 message=b'Unsupported LFS transfer method: %s' % method,
368 )
368 )
369 return True
369 return True
@@ -1,548 +1,548
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
8
9 import hashlib
9 import hashlib
10
10
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial.node import bin, hex, short
12 from mercurial.node import bin, hex, short
13 from mercurial.pycompat import (
13 from mercurial.pycompat import (
14 getattr,
14 getattr,
15 setattr,
15 setattr,
16 )
16 )
17
17
18 from mercurial import (
18 from mercurial import (
19 bundle2,
19 bundle2,
20 changegroup,
20 changegroup,
21 cmdutil,
21 cmdutil,
22 context,
22 context,
23 error,
23 error,
24 exchange,
24 exchange,
25 exthelper,
25 exthelper,
26 localrepo,
26 localrepo,
27 revlog,
27 revlog,
28 scmutil,
28 scmutil,
29 util,
29 util,
30 vfs as vfsmod,
30 vfs as vfsmod,
31 wireprotov1server,
31 wireprotov1server,
32 )
32 )
33
33
34 from mercurial.upgrade_utils import (
34 from mercurial.upgrade_utils import (
35 actions as upgrade_actions,
35 actions as upgrade_actions,
36 engine as upgrade_engine,
36 engine as upgrade_engine,
37 )
37 )
38
38
39 from mercurial.interfaces import repository
39 from mercurial.interfaces import repository
40
40
41 from mercurial.utils import (
41 from mercurial.utils import (
42 storageutil,
42 storageutil,
43 stringutil,
43 stringutil,
44 )
44 )
45
45
46 from ..largefiles import lfutil
46 from ..largefiles import lfutil
47
47
48 from . import (
48 from . import (
49 blobstore,
49 blobstore,
50 pointer,
50 pointer,
51 )
51 )
52
52
53 eh = exthelper.exthelper()
53 eh = exthelper.exthelper()
54
54
55
55
56 @eh.wrapfunction(localrepo, b'makefilestorage')
56 @eh.wrapfunction(localrepo, 'makefilestorage')
57 def localrepomakefilestorage(orig, requirements, features, **kwargs):
57 def localrepomakefilestorage(orig, requirements, features, **kwargs):
58 if b'lfs' in requirements:
58 if b'lfs' in requirements:
59 features.add(repository.REPO_FEATURE_LFS)
59 features.add(repository.REPO_FEATURE_LFS)
60
60
61 return orig(requirements=requirements, features=features, **kwargs)
61 return orig(requirements=requirements, features=features, **kwargs)
62
62
63
63
64 @eh.wrapfunction(changegroup, b'allsupportedversions')
64 @eh.wrapfunction(changegroup, 'allsupportedversions')
65 def allsupportedversions(orig, ui):
65 def allsupportedversions(orig, ui):
66 versions = orig(ui)
66 versions = orig(ui)
67 versions.add(b'03')
67 versions.add(b'03')
68 return versions
68 return versions
69
69
70
70
71 @eh.wrapfunction(wireprotov1server, b'_capabilities')
71 @eh.wrapfunction(wireprotov1server, '_capabilities')
72 def _capabilities(orig, repo, proto):
72 def _capabilities(orig, repo, proto):
73 '''Wrap server command to announce lfs server capability'''
73 '''Wrap server command to announce lfs server capability'''
74 caps = orig(repo, proto)
74 caps = orig(repo, proto)
75 if util.safehasattr(repo.svfs, b'lfslocalblobstore'):
75 if util.safehasattr(repo.svfs, b'lfslocalblobstore'):
76 # Advertise a slightly different capability when lfs is *required*, so
76 # Advertise a slightly different capability when lfs is *required*, so
77 # that the client knows it MUST load the extension. If lfs is not
77 # that the client knows it MUST load the extension. If lfs is not
78 # required on the server, there's no reason to autoload the extension
78 # required on the server, there's no reason to autoload the extension
79 # on the client.
79 # on the client.
80 if b'lfs' in repo.requirements:
80 if b'lfs' in repo.requirements:
81 caps.append(b'lfs-serve')
81 caps.append(b'lfs-serve')
82
82
83 caps.append(b'lfs')
83 caps.append(b'lfs')
84 return caps
84 return caps
85
85
86
86
87 def bypasscheckhash(self, text):
87 def bypasscheckhash(self, text):
88 return False
88 return False
89
89
90
90
91 def readfromstore(self, text):
91 def readfromstore(self, text):
92 """Read filelog content from local blobstore transform for flagprocessor.
92 """Read filelog content from local blobstore transform for flagprocessor.
93
93
94 Default tranform for flagprocessor, returning contents from blobstore.
94 Default tranform for flagprocessor, returning contents from blobstore.
95 Returns a 2-typle (text, validatehash) where validatehash is True as the
95 Returns a 2-typle (text, validatehash) where validatehash is True as the
96 contents of the blobstore should be checked using checkhash.
96 contents of the blobstore should be checked using checkhash.
97 """
97 """
98 p = pointer.deserialize(text)
98 p = pointer.deserialize(text)
99 oid = p.oid()
99 oid = p.oid()
100 store = self.opener.lfslocalblobstore
100 store = self.opener.lfslocalblobstore
101 if not store.has(oid):
101 if not store.has(oid):
102 p.filename = self.filename
102 p.filename = self.filename
103 self.opener.lfsremoteblobstore.readbatch([p], store)
103 self.opener.lfsremoteblobstore.readbatch([p], store)
104
104
105 # The caller will validate the content
105 # The caller will validate the content
106 text = store.read(oid, verify=False)
106 text = store.read(oid, verify=False)
107
107
108 # pack hg filelog metadata
108 # pack hg filelog metadata
109 hgmeta = {}
109 hgmeta = {}
110 for k in p.keys():
110 for k in p.keys():
111 if k.startswith(b'x-hg-'):
111 if k.startswith(b'x-hg-'):
112 name = k[len(b'x-hg-') :]
112 name = k[len(b'x-hg-') :]
113 hgmeta[name] = p[k]
113 hgmeta[name] = p[k]
114 if hgmeta or text.startswith(b'\1\n'):
114 if hgmeta or text.startswith(b'\1\n'):
115 text = storageutil.packmeta(hgmeta, text)
115 text = storageutil.packmeta(hgmeta, text)
116
116
117 return (text, True)
117 return (text, True)
118
118
119
119
120 def writetostore(self, text):
120 def writetostore(self, text):
121 # hg filelog metadata (includes rename, etc)
121 # hg filelog metadata (includes rename, etc)
122 hgmeta, offset = storageutil.parsemeta(text)
122 hgmeta, offset = storageutil.parsemeta(text)
123 if offset and offset > 0:
123 if offset and offset > 0:
124 # lfs blob does not contain hg filelog metadata
124 # lfs blob does not contain hg filelog metadata
125 text = text[offset:]
125 text = text[offset:]
126
126
127 # git-lfs only supports sha256
127 # git-lfs only supports sha256
128 oid = hex(hashlib.sha256(text).digest())
128 oid = hex(hashlib.sha256(text).digest())
129 self.opener.lfslocalblobstore.write(oid, text)
129 self.opener.lfslocalblobstore.write(oid, text)
130
130
131 # replace contents with metadata
131 # replace contents with metadata
132 longoid = b'sha256:%s' % oid
132 longoid = b'sha256:%s' % oid
133 metadata = pointer.gitlfspointer(oid=longoid, size=b'%d' % len(text))
133 metadata = pointer.gitlfspointer(oid=longoid, size=b'%d' % len(text))
134
134
135 # by default, we expect the content to be binary. however, LFS could also
135 # by default, we expect the content to be binary. however, LFS could also
136 # be used for non-binary content. add a special entry for non-binary data.
136 # be used for non-binary content. add a special entry for non-binary data.
137 # this will be used by filectx.isbinary().
137 # this will be used by filectx.isbinary().
138 if not stringutil.binary(text):
138 if not stringutil.binary(text):
139 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
139 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
140 metadata[b'x-is-binary'] = b'0'
140 metadata[b'x-is-binary'] = b'0'
141
141
142 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
142 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
143 if hgmeta is not None:
143 if hgmeta is not None:
144 for k, v in hgmeta.items():
144 for k, v in hgmeta.items():
145 metadata[b'x-hg-%s' % k] = v
145 metadata[b'x-hg-%s' % k] = v
146
146
147 rawtext = metadata.serialize()
147 rawtext = metadata.serialize()
148 return (rawtext, False)
148 return (rawtext, False)
149
149
150
150
151 def _islfs(rlog, node=None, rev=None):
151 def _islfs(rlog, node=None, rev=None):
152 if rev is None:
152 if rev is None:
153 if node is None:
153 if node is None:
154 # both None - likely working copy content where node is not ready
154 # both None - likely working copy content where node is not ready
155 return False
155 return False
156 rev = rlog.rev(node)
156 rev = rlog.rev(node)
157 else:
157 else:
158 node = rlog.node(rev)
158 node = rlog.node(rev)
159 if node == rlog.nullid:
159 if node == rlog.nullid:
160 return False
160 return False
161 flags = rlog.flags(rev)
161 flags = rlog.flags(rev)
162 return bool(flags & revlog.REVIDX_EXTSTORED)
162 return bool(flags & revlog.REVIDX_EXTSTORED)
163
163
164
164
165 # Wrapping may also be applied by remotefilelog
165 # Wrapping may also be applied by remotefilelog
166 def filelogaddrevision(
166 def filelogaddrevision(
167 orig,
167 orig,
168 self,
168 self,
169 text,
169 text,
170 transaction,
170 transaction,
171 link,
171 link,
172 p1,
172 p1,
173 p2,
173 p2,
174 cachedelta=None,
174 cachedelta=None,
175 node=None,
175 node=None,
176 flags=revlog.REVIDX_DEFAULT_FLAGS,
176 flags=revlog.REVIDX_DEFAULT_FLAGS,
177 **kwds
177 **kwds
178 ):
178 ):
179 # The matcher isn't available if reposetup() wasn't called.
179 # The matcher isn't available if reposetup() wasn't called.
180 lfstrack = self._revlog.opener.options.get(b'lfstrack')
180 lfstrack = self._revlog.opener.options.get(b'lfstrack')
181
181
182 if lfstrack:
182 if lfstrack:
183 textlen = len(text)
183 textlen = len(text)
184 # exclude hg rename meta from file size
184 # exclude hg rename meta from file size
185 meta, offset = storageutil.parsemeta(text)
185 meta, offset = storageutil.parsemeta(text)
186 if offset:
186 if offset:
187 textlen -= offset
187 textlen -= offset
188
188
189 if lfstrack(self._revlog.filename, textlen):
189 if lfstrack(self._revlog.filename, textlen):
190 flags |= revlog.REVIDX_EXTSTORED
190 flags |= revlog.REVIDX_EXTSTORED
191
191
192 return orig(
192 return orig(
193 self,
193 self,
194 text,
194 text,
195 transaction,
195 transaction,
196 link,
196 link,
197 p1,
197 p1,
198 p2,
198 p2,
199 cachedelta=cachedelta,
199 cachedelta=cachedelta,
200 node=node,
200 node=node,
201 flags=flags,
201 flags=flags,
202 **kwds
202 **kwds
203 )
203 )
204
204
205
205
206 # Wrapping may also be applied by remotefilelog
206 # Wrapping may also be applied by remotefilelog
207 def filelogrenamed(orig, self, node):
207 def filelogrenamed(orig, self, node):
208 if _islfs(self._revlog, node):
208 if _islfs(self._revlog, node):
209 rawtext = self._revlog.rawdata(node)
209 rawtext = self._revlog.rawdata(node)
210 if not rawtext:
210 if not rawtext:
211 return False
211 return False
212 metadata = pointer.deserialize(rawtext)
212 metadata = pointer.deserialize(rawtext)
213 if b'x-hg-copy' in metadata and b'x-hg-copyrev' in metadata:
213 if b'x-hg-copy' in metadata and b'x-hg-copyrev' in metadata:
214 return metadata[b'x-hg-copy'], bin(metadata[b'x-hg-copyrev'])
214 return metadata[b'x-hg-copy'], bin(metadata[b'x-hg-copyrev'])
215 else:
215 else:
216 return False
216 return False
217 return orig(self, node)
217 return orig(self, node)
218
218
219
219
220 # Wrapping may also be applied by remotefilelog
220 # Wrapping may also be applied by remotefilelog
221 def filelogsize(orig, self, rev):
221 def filelogsize(orig, self, rev):
222 if _islfs(self._revlog, rev=rev):
222 if _islfs(self._revlog, rev=rev):
223 # fast path: use lfs metadata to answer size
223 # fast path: use lfs metadata to answer size
224 rawtext = self._revlog.rawdata(rev)
224 rawtext = self._revlog.rawdata(rev)
225 metadata = pointer.deserialize(rawtext)
225 metadata = pointer.deserialize(rawtext)
226 return int(metadata[b'size'])
226 return int(metadata[b'size'])
227 return orig(self, rev)
227 return orig(self, rev)
228
228
229
229
230 @eh.wrapfunction(revlog, b'_verify_revision')
230 @eh.wrapfunction(revlog, '_verify_revision')
231 def _verify_revision(orig, rl, skipflags, state, node):
231 def _verify_revision(orig, rl, skipflags, state, node):
232 if _islfs(rl, node=node):
232 if _islfs(rl, node=node):
233 rawtext = rl.rawdata(node)
233 rawtext = rl.rawdata(node)
234 metadata = pointer.deserialize(rawtext)
234 metadata = pointer.deserialize(rawtext)
235
235
236 # Don't skip blobs that are stored locally, as local verification is
236 # Don't skip blobs that are stored locally, as local verification is
237 # relatively cheap and there's no other way to verify the raw data in
237 # relatively cheap and there's no other way to verify the raw data in
238 # the revlog.
238 # the revlog.
239 if rl.opener.lfslocalblobstore.has(metadata.oid()):
239 if rl.opener.lfslocalblobstore.has(metadata.oid()):
240 skipflags &= ~revlog.REVIDX_EXTSTORED
240 skipflags &= ~revlog.REVIDX_EXTSTORED
241 elif skipflags & revlog.REVIDX_EXTSTORED:
241 elif skipflags & revlog.REVIDX_EXTSTORED:
242 # The wrapped method will set `skipread`, but there's enough local
242 # The wrapped method will set `skipread`, but there's enough local
243 # info to check renames.
243 # info to check renames.
244 state[b'safe_renamed'].add(node)
244 state[b'safe_renamed'].add(node)
245
245
246 orig(rl, skipflags, state, node)
246 orig(rl, skipflags, state, node)
247
247
248
248
249 @eh.wrapfunction(context.basefilectx, b'cmp')
249 @eh.wrapfunction(context.basefilectx, 'cmp')
250 def filectxcmp(orig, self, fctx):
250 def filectxcmp(orig, self, fctx):
251 """returns True if text is different than fctx"""
251 """returns True if text is different than fctx"""
252 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
252 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
253 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
253 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
254 # fast path: check LFS oid
254 # fast path: check LFS oid
255 p1 = pointer.deserialize(self.rawdata())
255 p1 = pointer.deserialize(self.rawdata())
256 p2 = pointer.deserialize(fctx.rawdata())
256 p2 = pointer.deserialize(fctx.rawdata())
257 return p1.oid() != p2.oid()
257 return p1.oid() != p2.oid()
258 return orig(self, fctx)
258 return orig(self, fctx)
259
259
260
260
261 @eh.wrapfunction(context.basefilectx, b'isbinary')
261 @eh.wrapfunction(context.basefilectx, 'isbinary')
262 def filectxisbinary(orig, self):
262 def filectxisbinary(orig, self):
263 if self.islfs():
263 if self.islfs():
264 # fast path: use lfs metadata to answer isbinary
264 # fast path: use lfs metadata to answer isbinary
265 metadata = pointer.deserialize(self.rawdata())
265 metadata = pointer.deserialize(self.rawdata())
266 # if lfs metadata says nothing, assume it's binary by default
266 # if lfs metadata says nothing, assume it's binary by default
267 return bool(int(metadata.get(b'x-is-binary', 1)))
267 return bool(int(metadata.get(b'x-is-binary', 1)))
268 return orig(self)
268 return orig(self)
269
269
270
270
271 def filectxislfs(self):
271 def filectxislfs(self):
272 return _islfs(self.filelog()._revlog, self.filenode())
272 return _islfs(self.filelog()._revlog, self.filenode())
273
273
274
274
275 @eh.wrapfunction(cmdutil, b'_updatecatformatter')
275 @eh.wrapfunction(cmdutil, '_updatecatformatter')
276 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
276 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
277 orig(fm, ctx, matcher, path, decode)
277 orig(fm, ctx, matcher, path, decode)
278 fm.data(rawdata=ctx[path].rawdata())
278 fm.data(rawdata=ctx[path].rawdata())
279
279
280
280
281 @eh.wrapfunction(scmutil, b'wrapconvertsink')
281 @eh.wrapfunction(scmutil, 'wrapconvertsink')
282 def convertsink(orig, sink):
282 def convertsink(orig, sink):
283 sink = orig(sink)
283 sink = orig(sink)
284 if sink.repotype == b'hg':
284 if sink.repotype == b'hg':
285
285
286 class lfssink(sink.__class__):
286 class lfssink(sink.__class__):
287 def putcommit(
287 def putcommit(
288 self,
288 self,
289 files,
289 files,
290 copies,
290 copies,
291 parents,
291 parents,
292 commit,
292 commit,
293 source,
293 source,
294 revmap,
294 revmap,
295 full,
295 full,
296 cleanp2,
296 cleanp2,
297 ):
297 ):
298 pc = super(lfssink, self).putcommit
298 pc = super(lfssink, self).putcommit
299 node = pc(
299 node = pc(
300 files,
300 files,
301 copies,
301 copies,
302 parents,
302 parents,
303 commit,
303 commit,
304 source,
304 source,
305 revmap,
305 revmap,
306 full,
306 full,
307 cleanp2,
307 cleanp2,
308 )
308 )
309
309
310 if b'lfs' not in self.repo.requirements:
310 if b'lfs' not in self.repo.requirements:
311 ctx = self.repo[node]
311 ctx = self.repo[node]
312
312
313 # The file list may contain removed files, so check for
313 # The file list may contain removed files, so check for
314 # membership before assuming it is in the context.
314 # membership before assuming it is in the context.
315 if any(f in ctx and ctx[f].islfs() for f, n in files):
315 if any(f in ctx and ctx[f].islfs() for f, n in files):
316 self.repo.requirements.add(b'lfs')
316 self.repo.requirements.add(b'lfs')
317 scmutil.writereporequirements(self.repo)
317 scmutil.writereporequirements(self.repo)
318
318
319 return node
319 return node
320
320
321 sink.__class__ = lfssink
321 sink.__class__ = lfssink
322
322
323 return sink
323 return sink
324
324
325
325
326 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
326 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
327 # options and blob stores are passed from othervfs to the new readonlyvfs.
327 # options and blob stores are passed from othervfs to the new readonlyvfs.
328 @eh.wrapfunction(vfsmod.readonlyvfs, b'__init__')
328 @eh.wrapfunction(vfsmod.readonlyvfs, '__init__')
329 def vfsinit(orig, self, othervfs):
329 def vfsinit(orig, self, othervfs):
330 orig(self, othervfs)
330 orig(self, othervfs)
331 # copy lfs related options
331 # copy lfs related options
332 for k, v in othervfs.options.items():
332 for k, v in othervfs.options.items():
333 if k.startswith(b'lfs'):
333 if k.startswith(b'lfs'):
334 self.options[k] = v
334 self.options[k] = v
335 # also copy lfs blobstores. note: this can run before reposetup, so lfs
335 # also copy lfs blobstores. note: this can run before reposetup, so lfs
336 # blobstore attributes are not always ready at this time.
336 # blobstore attributes are not always ready at this time.
337 for name in [b'lfslocalblobstore', b'lfsremoteblobstore']:
337 for name in [b'lfslocalblobstore', b'lfsremoteblobstore']:
338 if util.safehasattr(othervfs, name):
338 if util.safehasattr(othervfs, name):
339 setattr(self, name, getattr(othervfs, name))
339 setattr(self, name, getattr(othervfs, name))
340
340
341
341
342 def _prefetchfiles(repo, revmatches):
342 def _prefetchfiles(repo, revmatches):
343 """Ensure that required LFS blobs are present, fetching them as a group if
343 """Ensure that required LFS blobs are present, fetching them as a group if
344 needed."""
344 needed."""
345 if not util.safehasattr(repo.svfs, b'lfslocalblobstore'):
345 if not util.safehasattr(repo.svfs, b'lfslocalblobstore'):
346 return
346 return
347
347
348 pointers = []
348 pointers = []
349 oids = set()
349 oids = set()
350 localstore = repo.svfs.lfslocalblobstore
350 localstore = repo.svfs.lfslocalblobstore
351
351
352 for rev, match in revmatches:
352 for rev, match in revmatches:
353 ctx = repo[rev]
353 ctx = repo[rev]
354 for f in ctx.walk(match):
354 for f in ctx.walk(match):
355 p = pointerfromctx(ctx, f)
355 p = pointerfromctx(ctx, f)
356 if p and p.oid() not in oids and not localstore.has(p.oid()):
356 if p and p.oid() not in oids and not localstore.has(p.oid()):
357 p.filename = f
357 p.filename = f
358 pointers.append(p)
358 pointers.append(p)
359 oids.add(p.oid())
359 oids.add(p.oid())
360
360
361 if pointers:
361 if pointers:
362 # Recalculating the repo store here allows 'paths.default' that is set
362 # Recalculating the repo store here allows 'paths.default' that is set
363 # on the repo by a clone command to be used for the update.
363 # on the repo by a clone command to be used for the update.
364 blobstore.remote(repo).readbatch(pointers, localstore)
364 blobstore.remote(repo).readbatch(pointers, localstore)
365
365
366
366
367 def _canskipupload(repo):
367 def _canskipupload(repo):
368 # Skip if this hasn't been passed to reposetup()
368 # Skip if this hasn't been passed to reposetup()
369 if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'):
369 if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'):
370 return True
370 return True
371
371
372 # if remotestore is a null store, upload is a no-op and can be skipped
372 # if remotestore is a null store, upload is a no-op and can be skipped
373 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
373 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
374
374
375
375
376 def candownload(repo):
376 def candownload(repo):
377 # Skip if this hasn't been passed to reposetup()
377 # Skip if this hasn't been passed to reposetup()
378 if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'):
378 if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'):
379 return False
379 return False
380
380
381 # if remotestore is a null store, downloads will lead to nothing
381 # if remotestore is a null store, downloads will lead to nothing
382 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
382 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
383
383
384
384
385 def uploadblobsfromrevs(repo, revs):
385 def uploadblobsfromrevs(repo, revs):
386 """upload lfs blobs introduced by revs
386 """upload lfs blobs introduced by revs
387
387
388 Note: also used by other extensions e. g. infinitepush. avoid renaming.
388 Note: also used by other extensions e. g. infinitepush. avoid renaming.
389 """
389 """
390 if _canskipupload(repo):
390 if _canskipupload(repo):
391 return
391 return
392 pointers = extractpointers(repo, revs)
392 pointers = extractpointers(repo, revs)
393 uploadblobs(repo, pointers)
393 uploadblobs(repo, pointers)
394
394
395
395
396 def prepush(pushop):
396 def prepush(pushop):
397 """Prepush hook.
397 """Prepush hook.
398
398
399 Read through the revisions to push, looking for filelog entries that can be
399 Read through the revisions to push, looking for filelog entries that can be
400 deserialized into metadata so that we can block the push on their upload to
400 deserialized into metadata so that we can block the push on their upload to
401 the remote blobstore.
401 the remote blobstore.
402 """
402 """
403 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
403 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
404
404
405
405
406 @eh.wrapfunction(exchange, b'push')
406 @eh.wrapfunction(exchange, 'push')
407 def push(orig, repo, remote, *args, **kwargs):
407 def push(orig, repo, remote, *args, **kwargs):
408 """bail on push if the extension isn't enabled on remote when needed, and
408 """bail on push if the extension isn't enabled on remote when needed, and
409 update the remote store based on the destination path."""
409 update the remote store based on the destination path."""
410 if b'lfs' in repo.requirements:
410 if b'lfs' in repo.requirements:
411 # If the remote peer is for a local repo, the requirement tests in the
411 # If the remote peer is for a local repo, the requirement tests in the
412 # base class method enforce lfs support. Otherwise, some revisions in
412 # base class method enforce lfs support. Otherwise, some revisions in
413 # this repo use lfs, and the remote repo needs the extension loaded.
413 # this repo use lfs, and the remote repo needs the extension loaded.
414 if not remote.local() and not remote.capable(b'lfs'):
414 if not remote.local() and not remote.capable(b'lfs'):
415 # This is a copy of the message in exchange.push() when requirements
415 # This is a copy of the message in exchange.push() when requirements
416 # are missing between local repos.
416 # are missing between local repos.
417 m = _(b"required features are not supported in the destination: %s")
417 m = _(b"required features are not supported in the destination: %s")
418 raise error.Abort(
418 raise error.Abort(
419 m % b'lfs', hint=_(b'enable the lfs extension on the server')
419 m % b'lfs', hint=_(b'enable the lfs extension on the server')
420 )
420 )
421
421
422 # Repositories where this extension is disabled won't have the field.
422 # Repositories where this extension is disabled won't have the field.
423 # But if there's a requirement, then the extension must be loaded AND
423 # But if there's a requirement, then the extension must be loaded AND
424 # there may be blobs to push.
424 # there may be blobs to push.
425 remotestore = repo.svfs.lfsremoteblobstore
425 remotestore = repo.svfs.lfsremoteblobstore
426 try:
426 try:
427 repo.svfs.lfsremoteblobstore = blobstore.remote(repo, remote.url())
427 repo.svfs.lfsremoteblobstore = blobstore.remote(repo, remote.url())
428 return orig(repo, remote, *args, **kwargs)
428 return orig(repo, remote, *args, **kwargs)
429 finally:
429 finally:
430 repo.svfs.lfsremoteblobstore = remotestore
430 repo.svfs.lfsremoteblobstore = remotestore
431 else:
431 else:
432 return orig(repo, remote, *args, **kwargs)
432 return orig(repo, remote, *args, **kwargs)
433
433
434
434
435 # when writing a bundle via "hg bundle" command, upload related LFS blobs
435 # when writing a bundle via "hg bundle" command, upload related LFS blobs
436 @eh.wrapfunction(bundle2, b'writenewbundle')
436 @eh.wrapfunction(bundle2, 'writenewbundle')
437 def writenewbundle(
437 def writenewbundle(
438 orig, ui, repo, source, filename, bundletype, outgoing, *args, **kwargs
438 orig, ui, repo, source, filename, bundletype, outgoing, *args, **kwargs
439 ):
439 ):
440 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
440 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
441 uploadblobsfromrevs(repo, outgoing.missing)
441 uploadblobsfromrevs(repo, outgoing.missing)
442 return orig(
442 return orig(
443 ui, repo, source, filename, bundletype, outgoing, *args, **kwargs
443 ui, repo, source, filename, bundletype, outgoing, *args, **kwargs
444 )
444 )
445
445
446
446
447 def extractpointers(repo, revs):
447 def extractpointers(repo, revs):
448 """return a list of lfs pointers added by given revs"""
448 """return a list of lfs pointers added by given revs"""
449 repo.ui.debug(b'lfs: computing set of blobs to upload\n')
449 repo.ui.debug(b'lfs: computing set of blobs to upload\n')
450 pointers = {}
450 pointers = {}
451
451
452 makeprogress = repo.ui.makeprogress
452 makeprogress = repo.ui.makeprogress
453 with makeprogress(
453 with makeprogress(
454 _(b'lfs search'), _(b'changesets'), len(revs)
454 _(b'lfs search'), _(b'changesets'), len(revs)
455 ) as progress:
455 ) as progress:
456 for r in revs:
456 for r in revs:
457 ctx = repo[r]
457 ctx = repo[r]
458 for p in pointersfromctx(ctx).values():
458 for p in pointersfromctx(ctx).values():
459 pointers[p.oid()] = p
459 pointers[p.oid()] = p
460 progress.increment()
460 progress.increment()
461 return sorted(pointers.values(), key=lambda p: p.oid())
461 return sorted(pointers.values(), key=lambda p: p.oid())
462
462
463
463
464 def pointerfromctx(ctx, f, removed=False):
464 def pointerfromctx(ctx, f, removed=False):
465 """return a pointer for the named file from the given changectx, or None if
465 """return a pointer for the named file from the given changectx, or None if
466 the file isn't LFS.
466 the file isn't LFS.
467
467
468 Optionally, the pointer for a file deleted from the context can be returned.
468 Optionally, the pointer for a file deleted from the context can be returned.
469 Since no such pointer is actually stored, and to distinguish from a non LFS
469 Since no such pointer is actually stored, and to distinguish from a non LFS
470 file, this pointer is represented by an empty dict.
470 file, this pointer is represented by an empty dict.
471 """
471 """
472 _ctx = ctx
472 _ctx = ctx
473 if f not in ctx:
473 if f not in ctx:
474 if not removed:
474 if not removed:
475 return None
475 return None
476 if f in ctx.p1():
476 if f in ctx.p1():
477 _ctx = ctx.p1()
477 _ctx = ctx.p1()
478 elif f in ctx.p2():
478 elif f in ctx.p2():
479 _ctx = ctx.p2()
479 _ctx = ctx.p2()
480 else:
480 else:
481 return None
481 return None
482 fctx = _ctx[f]
482 fctx = _ctx[f]
483 if not _islfs(fctx.filelog()._revlog, fctx.filenode()):
483 if not _islfs(fctx.filelog()._revlog, fctx.filenode()):
484 return None
484 return None
485 try:
485 try:
486 p = pointer.deserialize(fctx.rawdata())
486 p = pointer.deserialize(fctx.rawdata())
487 if ctx == _ctx:
487 if ctx == _ctx:
488 return p
488 return p
489 return {}
489 return {}
490 except pointer.InvalidPointer as ex:
490 except pointer.InvalidPointer as ex:
491 raise error.Abort(
491 raise error.Abort(
492 _(b'lfs: corrupted pointer (%s@%s): %s\n')
492 _(b'lfs: corrupted pointer (%s@%s): %s\n')
493 % (f, short(_ctx.node()), ex)
493 % (f, short(_ctx.node()), ex)
494 )
494 )
495
495
496
496
497 def pointersfromctx(ctx, removed=False):
497 def pointersfromctx(ctx, removed=False):
498 """return a dict {path: pointer} for given single changectx.
498 """return a dict {path: pointer} for given single changectx.
499
499
500 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
500 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
501 stored for the path is an empty dict.
501 stored for the path is an empty dict.
502 """
502 """
503 result = {}
503 result = {}
504 m = ctx.repo().narrowmatch()
504 m = ctx.repo().narrowmatch()
505
505
506 # TODO: consider manifest.fastread() instead
506 # TODO: consider manifest.fastread() instead
507 for f in ctx.files():
507 for f in ctx.files():
508 if not m(f):
508 if not m(f):
509 continue
509 continue
510 p = pointerfromctx(ctx, f, removed=removed)
510 p = pointerfromctx(ctx, f, removed=removed)
511 if p is not None:
511 if p is not None:
512 result[f] = p
512 result[f] = p
513 return result
513 return result
514
514
515
515
516 def uploadblobs(repo, pointers):
516 def uploadblobs(repo, pointers):
517 """upload given pointers from local blobstore"""
517 """upload given pointers from local blobstore"""
518 if not pointers:
518 if not pointers:
519 return
519 return
520
520
521 remoteblob = repo.svfs.lfsremoteblobstore
521 remoteblob = repo.svfs.lfsremoteblobstore
522 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
522 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
523
523
524
524
525 @eh.wrapfunction(upgrade_engine, b'finishdatamigration')
525 @eh.wrapfunction(upgrade_engine, 'finishdatamigration')
526 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
526 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
527 orig(ui, srcrepo, dstrepo, requirements)
527 orig(ui, srcrepo, dstrepo, requirements)
528
528
529 # Skip if this hasn't been passed to reposetup()
529 # Skip if this hasn't been passed to reposetup()
530 if util.safehasattr(
530 if util.safehasattr(
531 srcrepo.svfs, b'lfslocalblobstore'
531 srcrepo.svfs, b'lfslocalblobstore'
532 ) and util.safehasattr(dstrepo.svfs, b'lfslocalblobstore'):
532 ) and util.safehasattr(dstrepo.svfs, b'lfslocalblobstore'):
533 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
533 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
534 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
534 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
535
535
536 for dirpath, dirs, files in srclfsvfs.walk():
536 for dirpath, dirs, files in srclfsvfs.walk():
537 for oid in files:
537 for oid in files:
538 ui.write(_(b'copying lfs blob %s\n') % oid)
538 ui.write(_(b'copying lfs blob %s\n') % oid)
539 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
539 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
540
540
541
541
542 @eh.wrapfunction(upgrade_actions, b'preservedrequirements')
542 @eh.wrapfunction(upgrade_actions, 'preservedrequirements')
543 @eh.wrapfunction(upgrade_actions, b'supporteddestrequirements')
543 @eh.wrapfunction(upgrade_actions, 'supporteddestrequirements')
544 def upgraderequirements(orig, repo):
544 def upgraderequirements(orig, repo):
545 reqs = orig(repo)
545 reqs = orig(repo)
546 if b'lfs' in repo.requirements:
546 if b'lfs' in repo.requirements:
547 reqs.add(b'lfs')
547 reqs.add(b'lfs')
548 return reqs
548 return reqs
General Comments 0
You need to be logged in to leave comments. Login now