##// END OF EJS Templates
lfs: fix the stall and corruption issue when concurrently uploading blobs...
Matt Harbison -
r44746:43eea17a default
parent child Browse files
Show More
@@ -1,776 +1,765 b''
1 # blobstore.py - local and remote (speaking Git-LFS protocol) blob storages
1 # blobstore.py - local and remote (speaking Git-LFS protocol) blob storages
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 contextlib
10 import contextlib
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import json
13 import json
14 import os
14 import os
15 import re
15 import re
16 import socket
16 import socket
17
17
18 from mercurial.i18n import _
18 from mercurial.i18n import _
19 from mercurial.pycompat import getattr
19 from mercurial.pycompat import getattr
20
20
21 from mercurial import (
21 from mercurial import (
22 encoding,
22 encoding,
23 error,
23 error,
24 httpconnection as httpconnectionmod,
24 node,
25 node,
25 pathutil,
26 pathutil,
26 pycompat,
27 pycompat,
27 url as urlmod,
28 url as urlmod,
28 util,
29 util,
29 vfs as vfsmod,
30 vfs as vfsmod,
30 worker,
31 worker,
31 )
32 )
32
33
33 from mercurial.utils import stringutil
34 from mercurial.utils import stringutil
34
35
35 from ..largefiles import lfutil
36 from ..largefiles import lfutil
36
37
37 # 64 bytes for SHA256
38 # 64 bytes for SHA256
38 _lfsre = re.compile(br'\A[a-f0-9]{64}\Z')
39 _lfsre = re.compile(br'\A[a-f0-9]{64}\Z')
39
40
40
41
41 class lfsvfs(vfsmod.vfs):
42 class lfsvfs(vfsmod.vfs):
42 def join(self, path):
43 def join(self, path):
43 """split the path at first two characters, like: XX/XXXXX..."""
44 """split the path at first two characters, like: XX/XXXXX..."""
44 if not _lfsre.match(path):
45 if not _lfsre.match(path):
45 raise error.ProgrammingError(b'unexpected lfs path: %s' % path)
46 raise error.ProgrammingError(b'unexpected lfs path: %s' % path)
46 return super(lfsvfs, self).join(path[0:2], path[2:])
47 return super(lfsvfs, self).join(path[0:2], path[2:])
47
48
48 def walk(self, path=None, onerror=None):
49 def walk(self, path=None, onerror=None):
49 """Yield (dirpath, [], oids) tuple for blobs under path
50 """Yield (dirpath, [], oids) tuple for blobs under path
50
51
51 Oids only exist in the root of this vfs, so dirpath is always ''.
52 Oids only exist in the root of this vfs, so dirpath is always ''.
52 """
53 """
53 root = os.path.normpath(self.base)
54 root = os.path.normpath(self.base)
54 # when dirpath == root, dirpath[prefixlen:] becomes empty
55 # when dirpath == root, dirpath[prefixlen:] becomes empty
55 # because len(dirpath) < prefixlen.
56 # because len(dirpath) < prefixlen.
56 prefixlen = len(pathutil.normasprefix(root))
57 prefixlen = len(pathutil.normasprefix(root))
57 oids = []
58 oids = []
58
59
59 for dirpath, dirs, files in os.walk(
60 for dirpath, dirs, files in os.walk(
60 self.reljoin(self.base, path or b''), onerror=onerror
61 self.reljoin(self.base, path or b''), onerror=onerror
61 ):
62 ):
62 dirpath = dirpath[prefixlen:]
63 dirpath = dirpath[prefixlen:]
63
64
64 # Silently skip unexpected files and directories
65 # Silently skip unexpected files and directories
65 if len(dirpath) == 2:
66 if len(dirpath) == 2:
66 oids.extend(
67 oids.extend(
67 [dirpath + f for f in files if _lfsre.match(dirpath + f)]
68 [dirpath + f for f in files if _lfsre.match(dirpath + f)]
68 )
69 )
69
70
70 yield (b'', [], oids)
71 yield (b'', [], oids)
71
72
72
73
73 class nullvfs(lfsvfs):
74 class nullvfs(lfsvfs):
74 def __init__(self):
75 def __init__(self):
75 pass
76 pass
76
77
77 def exists(self, oid):
78 def exists(self, oid):
78 return False
79 return False
79
80
80 def read(self, oid):
81 def read(self, oid):
81 # store.read() calls into here if the blob doesn't exist in its
82 # store.read() calls into here if the blob doesn't exist in its
82 # self.vfs. Raise the same error as a normal vfs when asked to read a
83 # self.vfs. Raise the same error as a normal vfs when asked to read a
83 # file that doesn't exist. The only difference is the full file path
84 # file that doesn't exist. The only difference is the full file path
84 # isn't available in the error.
85 # isn't available in the error.
85 raise IOError(
86 raise IOError(
86 errno.ENOENT,
87 errno.ENOENT,
87 pycompat.sysstr(b'%s: No such file or directory' % oid),
88 pycompat.sysstr(b'%s: No such file or directory' % oid),
88 )
89 )
89
90
90 def walk(self, path=None, onerror=None):
91 def walk(self, path=None, onerror=None):
91 return (b'', [], [])
92 return (b'', [], [])
92
93
93 def write(self, oid, data):
94 def write(self, oid, data):
94 pass
95 pass
95
96
96
97
97 class lfsuploadfile(object):
98 class lfsuploadfile(httpconnectionmod.httpsendfile):
98 """a file-like object that supports __len__ and read.
99 """a file-like object that supports keepalive.
99 """
100 """
100
101
101 def __init__(self, fp):
102 def __init__(self, ui, filename):
102 self._fp = fp
103 super(lfsuploadfile, self).__init__(ui, filename, b'rb')
103 fp.seek(0, os.SEEK_END)
104 self.read = self._data.read
104 self._len = fp.tell()
105 fp.seek(0)
106
107 def __len__(self):
108 return self._len
109
105
110 def read(self, size):
106 def _makeprogress(self):
111 if self._fp is None:
107 return None # progress is handled by the worker client
112 return b''
113 return self._fp.read(size)
114
115 def close(self):
116 if self._fp is not None:
117 self._fp.close()
118 self._fp = None
119
108
120
109
121 class local(object):
110 class local(object):
122 """Local blobstore for large file contents.
111 """Local blobstore for large file contents.
123
112
124 This blobstore is used both as a cache and as a staging area for large blobs
113 This blobstore is used both as a cache and as a staging area for large blobs
125 to be uploaded to the remote blobstore.
114 to be uploaded to the remote blobstore.
126 """
115 """
127
116
128 def __init__(self, repo):
117 def __init__(self, repo):
129 fullpath = repo.svfs.join(b'lfs/objects')
118 fullpath = repo.svfs.join(b'lfs/objects')
130 self.vfs = lfsvfs(fullpath)
119 self.vfs = lfsvfs(fullpath)
131
120
132 if repo.ui.configbool(b'experimental', b'lfs.disableusercache'):
121 if repo.ui.configbool(b'experimental', b'lfs.disableusercache'):
133 self.cachevfs = nullvfs()
122 self.cachevfs = nullvfs()
134 else:
123 else:
135 usercache = lfutil._usercachedir(repo.ui, b'lfs')
124 usercache = lfutil._usercachedir(repo.ui, b'lfs')
136 self.cachevfs = lfsvfs(usercache)
125 self.cachevfs = lfsvfs(usercache)
137 self.ui = repo.ui
126 self.ui = repo.ui
138
127
139 def open(self, oid):
128 def open(self, oid):
140 """Open a read-only file descriptor to the named blob, in either the
129 """Open a read-only file descriptor to the named blob, in either the
141 usercache or the local store."""
130 usercache or the local store."""
142 return open(self.path(oid), b'rb')
131 return open(self.path(oid), b'rb')
143
132
144 def path(self, oid):
133 def path(self, oid):
145 """Build the path for the given blob ``oid``.
134 """Build the path for the given blob ``oid``.
146
135
147 If the blob exists locally, the path may point to either the usercache
136 If the blob exists locally, the path may point to either the usercache
148 or the local store. If it doesn't, it will point to the local store.
137 or the local store. If it doesn't, it will point to the local store.
149 This is meant for situations where existing code that isn't LFS aware
138 This is meant for situations where existing code that isn't LFS aware
150 needs to open a blob. Generally, prefer the ``open`` method on this
139 needs to open a blob. Generally, prefer the ``open`` method on this
151 class.
140 class.
152 """
141 """
153 # The usercache is the most likely place to hold the file. Commit will
142 # The usercache is the most likely place to hold the file. Commit will
154 # write to both it and the local store, as will anything that downloads
143 # write to both it and the local store, as will anything that downloads
155 # the blobs. However, things like clone without an update won't
144 # the blobs. However, things like clone without an update won't
156 # populate the local store. For an init + push of a local clone,
145 # populate the local store. For an init + push of a local clone,
157 # the usercache is the only place it _could_ be. If not present, the
146 # the usercache is the only place it _could_ be. If not present, the
158 # missing file msg here will indicate the local repo, not the usercache.
147 # missing file msg here will indicate the local repo, not the usercache.
159 if self.cachevfs.exists(oid):
148 if self.cachevfs.exists(oid):
160 return self.cachevfs.join(oid)
149 return self.cachevfs.join(oid)
161
150
162 return self.vfs.join(oid)
151 return self.vfs.join(oid)
163
152
164 def download(self, oid, src, content_length):
153 def download(self, oid, src, content_length):
165 """Read the blob from the remote source in chunks, verify the content,
154 """Read the blob from the remote source in chunks, verify the content,
166 and write to this local blobstore."""
155 and write to this local blobstore."""
167 sha256 = hashlib.sha256()
156 sha256 = hashlib.sha256()
168 size = 0
157 size = 0
169
158
170 with self.vfs(oid, b'wb', atomictemp=True) as fp:
159 with self.vfs(oid, b'wb', atomictemp=True) as fp:
171 for chunk in util.filechunkiter(src, size=1048576):
160 for chunk in util.filechunkiter(src, size=1048576):
172 fp.write(chunk)
161 fp.write(chunk)
173 sha256.update(chunk)
162 sha256.update(chunk)
174 size += len(chunk)
163 size += len(chunk)
175
164
176 # If the server advertised a length longer than what we actually
165 # If the server advertised a length longer than what we actually
177 # received, then we should expect that the server crashed while
166 # received, then we should expect that the server crashed while
178 # producing the response (but the server has no way of telling us
167 # producing the response (but the server has no way of telling us
179 # that), and we really don't need to try to write the response to
168 # that), and we really don't need to try to write the response to
180 # the localstore, because it's not going to match the expected.
169 # the localstore, because it's not going to match the expected.
181 if content_length is not None and int(content_length) != size:
170 if content_length is not None and int(content_length) != size:
182 msg = (
171 msg = (
183 b"Response length (%s) does not match Content-Length "
172 b"Response length (%s) does not match Content-Length "
184 b"header (%d): likely server-side crash"
173 b"header (%d): likely server-side crash"
185 )
174 )
186 raise LfsRemoteError(_(msg) % (size, int(content_length)))
175 raise LfsRemoteError(_(msg) % (size, int(content_length)))
187
176
188 realoid = node.hex(sha256.digest())
177 realoid = node.hex(sha256.digest())
189 if realoid != oid:
178 if realoid != oid:
190 raise LfsCorruptionError(
179 raise LfsCorruptionError(
191 _(b'corrupt remote lfs object: %s') % oid
180 _(b'corrupt remote lfs object: %s') % oid
192 )
181 )
193
182
194 self._linktousercache(oid)
183 self._linktousercache(oid)
195
184
196 def write(self, oid, data):
185 def write(self, oid, data):
197 """Write blob to local blobstore.
186 """Write blob to local blobstore.
198
187
199 This should only be called from the filelog during a commit or similar.
188 This should only be called from the filelog during a commit or similar.
200 As such, there is no need to verify the data. Imports from a remote
189 As such, there is no need to verify the data. Imports from a remote
201 store must use ``download()`` instead."""
190 store must use ``download()`` instead."""
202 with self.vfs(oid, b'wb', atomictemp=True) as fp:
191 with self.vfs(oid, b'wb', atomictemp=True) as fp:
203 fp.write(data)
192 fp.write(data)
204
193
205 self._linktousercache(oid)
194 self._linktousercache(oid)
206
195
207 def linkfromusercache(self, oid):
196 def linkfromusercache(self, oid):
208 """Link blobs found in the user cache into this store.
197 """Link blobs found in the user cache into this store.
209
198
210 The server module needs to do this when it lets the client know not to
199 The server module needs to do this when it lets the client know not to
211 upload the blob, to ensure it is always available in this store.
200 upload the blob, to ensure it is always available in this store.
212 Normally this is done implicitly when the client reads or writes the
201 Normally this is done implicitly when the client reads or writes the
213 blob, but that doesn't happen when the server tells the client that it
202 blob, but that doesn't happen when the server tells the client that it
214 already has the blob.
203 already has the blob.
215 """
204 """
216 if not isinstance(self.cachevfs, nullvfs) and not self.vfs.exists(oid):
205 if not isinstance(self.cachevfs, nullvfs) and not self.vfs.exists(oid):
217 self.ui.note(_(b'lfs: found %s in the usercache\n') % oid)
206 self.ui.note(_(b'lfs: found %s in the usercache\n') % oid)
218 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
207 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
219
208
220 def _linktousercache(self, oid):
209 def _linktousercache(self, oid):
221 # XXX: should we verify the content of the cache, and hardlink back to
210 # XXX: should we verify the content of the cache, and hardlink back to
222 # the local store on success, but truncate, write and link on failure?
211 # the local store on success, but truncate, write and link on failure?
223 if not self.cachevfs.exists(oid) and not isinstance(
212 if not self.cachevfs.exists(oid) and not isinstance(
224 self.cachevfs, nullvfs
213 self.cachevfs, nullvfs
225 ):
214 ):
226 self.ui.note(_(b'lfs: adding %s to the usercache\n') % oid)
215 self.ui.note(_(b'lfs: adding %s to the usercache\n') % oid)
227 lfutil.link(self.vfs.join(oid), self.cachevfs.join(oid))
216 lfutil.link(self.vfs.join(oid), self.cachevfs.join(oid))
228
217
229 def read(self, oid, verify=True):
218 def read(self, oid, verify=True):
230 """Read blob from local blobstore."""
219 """Read blob from local blobstore."""
231 if not self.vfs.exists(oid):
220 if not self.vfs.exists(oid):
232 blob = self._read(self.cachevfs, oid, verify)
221 blob = self._read(self.cachevfs, oid, verify)
233
222
234 # Even if revlog will verify the content, it needs to be verified
223 # Even if revlog will verify the content, it needs to be verified
235 # now before making the hardlink to avoid propagating corrupt blobs.
224 # now before making the hardlink to avoid propagating corrupt blobs.
236 # Don't abort if corruption is detected, because `hg verify` will
225 # Don't abort if corruption is detected, because `hg verify` will
237 # give more useful info about the corruption- simply don't add the
226 # give more useful info about the corruption- simply don't add the
238 # hardlink.
227 # hardlink.
239 if verify or node.hex(hashlib.sha256(blob).digest()) == oid:
228 if verify or node.hex(hashlib.sha256(blob).digest()) == oid:
240 self.ui.note(_(b'lfs: found %s in the usercache\n') % oid)
229 self.ui.note(_(b'lfs: found %s in the usercache\n') % oid)
241 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
230 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
242 else:
231 else:
243 self.ui.note(_(b'lfs: found %s in the local lfs store\n') % oid)
232 self.ui.note(_(b'lfs: found %s in the local lfs store\n') % oid)
244 blob = self._read(self.vfs, oid, verify)
233 blob = self._read(self.vfs, oid, verify)
245 return blob
234 return blob
246
235
247 def _read(self, vfs, oid, verify):
236 def _read(self, vfs, oid, verify):
248 """Read blob (after verifying) from the given store"""
237 """Read blob (after verifying) from the given store"""
249 blob = vfs.read(oid)
238 blob = vfs.read(oid)
250 if verify:
239 if verify:
251 _verify(oid, blob)
240 _verify(oid, blob)
252 return blob
241 return blob
253
242
254 def verify(self, oid):
243 def verify(self, oid):
255 """Indicate whether or not the hash of the underlying file matches its
244 """Indicate whether or not the hash of the underlying file matches its
256 name."""
245 name."""
257 sha256 = hashlib.sha256()
246 sha256 = hashlib.sha256()
258
247
259 with self.open(oid) as fp:
248 with self.open(oid) as fp:
260 for chunk in util.filechunkiter(fp, size=1048576):
249 for chunk in util.filechunkiter(fp, size=1048576):
261 sha256.update(chunk)
250 sha256.update(chunk)
262
251
263 return oid == node.hex(sha256.digest())
252 return oid == node.hex(sha256.digest())
264
253
265 def has(self, oid):
254 def has(self, oid):
266 """Returns True if the local blobstore contains the requested blob,
255 """Returns True if the local blobstore contains the requested blob,
267 False otherwise."""
256 False otherwise."""
268 return self.cachevfs.exists(oid) or self.vfs.exists(oid)
257 return self.cachevfs.exists(oid) or self.vfs.exists(oid)
269
258
270
259
271 def _urlerrorreason(urlerror):
260 def _urlerrorreason(urlerror):
272 '''Create a friendly message for the given URLError to be used in an
261 '''Create a friendly message for the given URLError to be used in an
273 LfsRemoteError message.
262 LfsRemoteError message.
274 '''
263 '''
275 inst = urlerror
264 inst = urlerror
276
265
277 if isinstance(urlerror.reason, Exception):
266 if isinstance(urlerror.reason, Exception):
278 inst = urlerror.reason
267 inst = urlerror.reason
279
268
280 if util.safehasattr(inst, b'reason'):
269 if util.safehasattr(inst, b'reason'):
281 try: # usually it is in the form (errno, strerror)
270 try: # usually it is in the form (errno, strerror)
282 reason = inst.reason.args[1]
271 reason = inst.reason.args[1]
283 except (AttributeError, IndexError):
272 except (AttributeError, IndexError):
284 # it might be anything, for example a string
273 # it might be anything, for example a string
285 reason = inst.reason
274 reason = inst.reason
286 if isinstance(reason, pycompat.unicode):
275 if isinstance(reason, pycompat.unicode):
287 # SSLError of Python 2.7.9 contains a unicode
276 # SSLError of Python 2.7.9 contains a unicode
288 reason = encoding.unitolocal(reason)
277 reason = encoding.unitolocal(reason)
289 return reason
278 return reason
290 elif getattr(inst, "strerror", None):
279 elif getattr(inst, "strerror", None):
291 return encoding.strtolocal(inst.strerror)
280 return encoding.strtolocal(inst.strerror)
292 else:
281 else:
293 return stringutil.forcebytestr(urlerror)
282 return stringutil.forcebytestr(urlerror)
294
283
295
284
296 class lfsauthhandler(util.urlreq.basehandler):
285 class lfsauthhandler(util.urlreq.basehandler):
297 handler_order = 480 # Before HTTPDigestAuthHandler (== 490)
286 handler_order = 480 # Before HTTPDigestAuthHandler (== 490)
298
287
299 def http_error_401(self, req, fp, code, msg, headers):
288 def http_error_401(self, req, fp, code, msg, headers):
300 """Enforces that any authentication performed is HTTP Basic
289 """Enforces that any authentication performed is HTTP Basic
301 Authentication. No authentication is also acceptable.
290 Authentication. No authentication is also acceptable.
302 """
291 """
303 authreq = headers.get('www-authenticate', None)
292 authreq = headers.get('www-authenticate', None)
304 if authreq:
293 if authreq:
305 scheme = authreq.split()[0]
294 scheme = authreq.split()[0]
306
295
307 if scheme.lower() != 'basic':
296 if scheme.lower() != 'basic':
308 msg = _(b'the server must support Basic Authentication')
297 msg = _(b'the server must support Basic Authentication')
309 raise util.urlerr.httperror(
298 raise util.urlerr.httperror(
310 req.get_full_url(),
299 req.get_full_url(),
311 code,
300 code,
312 encoding.strfromlocal(msg),
301 encoding.strfromlocal(msg),
313 headers,
302 headers,
314 fp,
303 fp,
315 )
304 )
316 return None
305 return None
317
306
318
307
319 class _gitlfsremote(object):
308 class _gitlfsremote(object):
320 def __init__(self, repo, url):
309 def __init__(self, repo, url):
321 ui = repo.ui
310 ui = repo.ui
322 self.ui = ui
311 self.ui = ui
323 baseurl, authinfo = url.authinfo()
312 baseurl, authinfo = url.authinfo()
324 self.baseurl = baseurl.rstrip(b'/')
313 self.baseurl = baseurl.rstrip(b'/')
325 useragent = repo.ui.config(b'experimental', b'lfs.user-agent')
314 useragent = repo.ui.config(b'experimental', b'lfs.user-agent')
326 if not useragent:
315 if not useragent:
327 useragent = b'git-lfs/2.3.4 (Mercurial %s)' % util.version()
316 useragent = b'git-lfs/2.3.4 (Mercurial %s)' % util.version()
328 self.urlopener = urlmod.opener(ui, authinfo, useragent)
317 self.urlopener = urlmod.opener(ui, authinfo, useragent)
329 self.urlopener.add_handler(lfsauthhandler())
318 self.urlopener.add_handler(lfsauthhandler())
330 self.retry = ui.configint(b'lfs', b'retry')
319 self.retry = ui.configint(b'lfs', b'retry')
331
320
332 def writebatch(self, pointers, fromstore):
321 def writebatch(self, pointers, fromstore):
333 """Batch upload from local to remote blobstore."""
322 """Batch upload from local to remote blobstore."""
334 self._batch(_deduplicate(pointers), fromstore, b'upload')
323 self._batch(_deduplicate(pointers), fromstore, b'upload')
335
324
336 def readbatch(self, pointers, tostore):
325 def readbatch(self, pointers, tostore):
337 """Batch download from remote to local blostore."""
326 """Batch download from remote to local blostore."""
338 self._batch(_deduplicate(pointers), tostore, b'download')
327 self._batch(_deduplicate(pointers), tostore, b'download')
339
328
340 def _batchrequest(self, pointers, action):
329 def _batchrequest(self, pointers, action):
341 """Get metadata about objects pointed by pointers for given action
330 """Get metadata about objects pointed by pointers for given action
342
331
343 Return decoded JSON object like {'objects': [{'oid': '', 'size': 1}]}
332 Return decoded JSON object like {'objects': [{'oid': '', 'size': 1}]}
344 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
333 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
345 """
334 """
346 objects = [
335 objects = [
347 {'oid': pycompat.strurl(p.oid()), 'size': p.size()}
336 {'oid': pycompat.strurl(p.oid()), 'size': p.size()}
348 for p in pointers
337 for p in pointers
349 ]
338 ]
350 requestdata = pycompat.bytesurl(
339 requestdata = pycompat.bytesurl(
351 json.dumps(
340 json.dumps(
352 {'objects': objects, 'operation': pycompat.strurl(action),}
341 {'objects': objects, 'operation': pycompat.strurl(action),}
353 )
342 )
354 )
343 )
355 url = b'%s/objects/batch' % self.baseurl
344 url = b'%s/objects/batch' % self.baseurl
356 batchreq = util.urlreq.request(pycompat.strurl(url), data=requestdata)
345 batchreq = util.urlreq.request(pycompat.strurl(url), data=requestdata)
357 batchreq.add_header('Accept', 'application/vnd.git-lfs+json')
346 batchreq.add_header('Accept', 'application/vnd.git-lfs+json')
358 batchreq.add_header('Content-Type', 'application/vnd.git-lfs+json')
347 batchreq.add_header('Content-Type', 'application/vnd.git-lfs+json')
359 try:
348 try:
360 with contextlib.closing(self.urlopener.open(batchreq)) as rsp:
349 with contextlib.closing(self.urlopener.open(batchreq)) as rsp:
361 rawjson = rsp.read()
350 rawjson = rsp.read()
362 except util.urlerr.httperror as ex:
351 except util.urlerr.httperror as ex:
363 hints = {
352 hints = {
364 400: _(
353 400: _(
365 b'check that lfs serving is enabled on %s and "%s" is '
354 b'check that lfs serving is enabled on %s and "%s" is '
366 b'supported'
355 b'supported'
367 )
356 )
368 % (self.baseurl, action),
357 % (self.baseurl, action),
369 404: _(b'the "lfs.url" config may be used to override %s')
358 404: _(b'the "lfs.url" config may be used to override %s')
370 % self.baseurl,
359 % self.baseurl,
371 }
360 }
372 hint = hints.get(ex.code, _(b'api=%s, action=%s') % (url, action))
361 hint = hints.get(ex.code, _(b'api=%s, action=%s') % (url, action))
373 raise LfsRemoteError(
362 raise LfsRemoteError(
374 _(b'LFS HTTP error: %s') % stringutil.forcebytestr(ex),
363 _(b'LFS HTTP error: %s') % stringutil.forcebytestr(ex),
375 hint=hint,
364 hint=hint,
376 )
365 )
377 except util.urlerr.urlerror as ex:
366 except util.urlerr.urlerror as ex:
378 hint = (
367 hint = (
379 _(b'the "lfs.url" config may be used to override %s')
368 _(b'the "lfs.url" config may be used to override %s')
380 % self.baseurl
369 % self.baseurl
381 )
370 )
382 raise LfsRemoteError(
371 raise LfsRemoteError(
383 _(b'LFS error: %s') % _urlerrorreason(ex), hint=hint
372 _(b'LFS error: %s') % _urlerrorreason(ex), hint=hint
384 )
373 )
385 try:
374 try:
386 response = pycompat.json_loads(rawjson)
375 response = pycompat.json_loads(rawjson)
387 except ValueError:
376 except ValueError:
388 raise LfsRemoteError(
377 raise LfsRemoteError(
389 _(b'LFS server returns invalid JSON: %s')
378 _(b'LFS server returns invalid JSON: %s')
390 % rawjson.encode("utf-8")
379 % rawjson.encode("utf-8")
391 )
380 )
392
381
393 if self.ui.debugflag:
382 if self.ui.debugflag:
394 self.ui.debug(b'Status: %d\n' % rsp.status)
383 self.ui.debug(b'Status: %d\n' % rsp.status)
395 # lfs-test-server and hg serve return headers in different order
384 # lfs-test-server and hg serve return headers in different order
396 headers = pycompat.bytestr(rsp.info()).strip()
385 headers = pycompat.bytestr(rsp.info()).strip()
397 self.ui.debug(b'%s\n' % b'\n'.join(sorted(headers.splitlines())))
386 self.ui.debug(b'%s\n' % b'\n'.join(sorted(headers.splitlines())))
398
387
399 if 'objects' in response:
388 if 'objects' in response:
400 response['objects'] = sorted(
389 response['objects'] = sorted(
401 response['objects'], key=lambda p: p['oid']
390 response['objects'], key=lambda p: p['oid']
402 )
391 )
403 self.ui.debug(
392 self.ui.debug(
404 b'%s\n'
393 b'%s\n'
405 % pycompat.bytesurl(
394 % pycompat.bytesurl(
406 json.dumps(
395 json.dumps(
407 response,
396 response,
408 indent=2,
397 indent=2,
409 separators=('', ': '),
398 separators=('', ': '),
410 sort_keys=True,
399 sort_keys=True,
411 )
400 )
412 )
401 )
413 )
402 )
414
403
415 def encodestr(x):
404 def encodestr(x):
416 if isinstance(x, pycompat.unicode):
405 if isinstance(x, pycompat.unicode):
417 return x.encode('utf-8')
406 return x.encode('utf-8')
418 return x
407 return x
419
408
420 return pycompat.rapply(encodestr, response)
409 return pycompat.rapply(encodestr, response)
421
410
422 def _checkforservererror(self, pointers, responses, action):
411 def _checkforservererror(self, pointers, responses, action):
423 """Scans errors from objects
412 """Scans errors from objects
424
413
425 Raises LfsRemoteError if any objects have an error"""
414 Raises LfsRemoteError if any objects have an error"""
426 for response in responses:
415 for response in responses:
427 # The server should return 404 when objects cannot be found. Some
416 # The server should return 404 when objects cannot be found. Some
428 # server implementation (ex. lfs-test-server) does not set "error"
417 # server implementation (ex. lfs-test-server) does not set "error"
429 # but just removes "download" from "actions". Treat that case
418 # but just removes "download" from "actions". Treat that case
430 # as the same as 404 error.
419 # as the same as 404 error.
431 if b'error' not in response:
420 if b'error' not in response:
432 if action == b'download' and action not in response.get(
421 if action == b'download' and action not in response.get(
433 b'actions', []
422 b'actions', []
434 ):
423 ):
435 code = 404
424 code = 404
436 else:
425 else:
437 continue
426 continue
438 else:
427 else:
439 # An error dict without a code doesn't make much sense, so
428 # An error dict without a code doesn't make much sense, so
440 # treat as a server error.
429 # treat as a server error.
441 code = response.get(b'error').get(b'code', 500)
430 code = response.get(b'error').get(b'code', 500)
442
431
443 ptrmap = {p.oid(): p for p in pointers}
432 ptrmap = {p.oid(): p for p in pointers}
444 p = ptrmap.get(response[b'oid'], None)
433 p = ptrmap.get(response[b'oid'], None)
445 if p:
434 if p:
446 filename = getattr(p, 'filename', b'unknown')
435 filename = getattr(p, 'filename', b'unknown')
447 errors = {
436 errors = {
448 404: b'The object does not exist',
437 404: b'The object does not exist',
449 410: b'The object was removed by the owner',
438 410: b'The object was removed by the owner',
450 422: b'Validation error',
439 422: b'Validation error',
451 500: b'Internal server error',
440 500: b'Internal server error',
452 }
441 }
453 msg = errors.get(code, b'status code %d' % code)
442 msg = errors.get(code, b'status code %d' % code)
454 raise LfsRemoteError(
443 raise LfsRemoteError(
455 _(b'LFS server error for "%s": %s') % (filename, msg)
444 _(b'LFS server error for "%s": %s') % (filename, msg)
456 )
445 )
457 else:
446 else:
458 raise LfsRemoteError(
447 raise LfsRemoteError(
459 _(b'LFS server error. Unsolicited response for oid %s')
448 _(b'LFS server error. Unsolicited response for oid %s')
460 % response[b'oid']
449 % response[b'oid']
461 )
450 )
462
451
463 def _extractobjects(self, response, pointers, action):
452 def _extractobjects(self, response, pointers, action):
464 """extract objects from response of the batch API
453 """extract objects from response of the batch API
465
454
466 response: parsed JSON object returned by batch API
455 response: parsed JSON object returned by batch API
467 return response['objects'] filtered by action
456 return response['objects'] filtered by action
468 raise if any object has an error
457 raise if any object has an error
469 """
458 """
470 # Scan errors from objects - fail early
459 # Scan errors from objects - fail early
471 objects = response.get(b'objects', [])
460 objects = response.get(b'objects', [])
472 self._checkforservererror(pointers, objects, action)
461 self._checkforservererror(pointers, objects, action)
473
462
474 # Filter objects with given action. Practically, this skips uploading
463 # Filter objects with given action. Practically, this skips uploading
475 # objects which exist in the server.
464 # objects which exist in the server.
476 filteredobjects = [
465 filteredobjects = [
477 o for o in objects if action in o.get(b'actions', [])
466 o for o in objects if action in o.get(b'actions', [])
478 ]
467 ]
479
468
480 return filteredobjects
469 return filteredobjects
481
470
482 def _basictransfer(self, obj, action, localstore):
471 def _basictransfer(self, obj, action, localstore):
483 """Download or upload a single object using basic transfer protocol
472 """Download or upload a single object using basic transfer protocol
484
473
485 obj: dict, an object description returned by batch API
474 obj: dict, an object description returned by batch API
486 action: string, one of ['upload', 'download']
475 action: string, one of ['upload', 'download']
487 localstore: blobstore.local
476 localstore: blobstore.local
488
477
489 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/\
478 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/\
490 basic-transfers.md
479 basic-transfers.md
491 """
480 """
492 oid = obj[b'oid']
481 oid = obj[b'oid']
493 href = obj[b'actions'][action].get(b'href')
482 href = obj[b'actions'][action].get(b'href')
494 headers = obj[b'actions'][action].get(b'header', {}).items()
483 headers = obj[b'actions'][action].get(b'header', {}).items()
495
484
496 request = util.urlreq.request(pycompat.strurl(href))
485 request = util.urlreq.request(pycompat.strurl(href))
497 if action == b'upload':
486 if action == b'upload':
498 # If uploading blobs, read data from local blobstore.
487 # If uploading blobs, read data from local blobstore.
499 if not localstore.verify(oid):
488 if not localstore.verify(oid):
500 raise error.Abort(
489 raise error.Abort(
501 _(b'detected corrupt lfs object: %s') % oid,
490 _(b'detected corrupt lfs object: %s') % oid,
502 hint=_(b'run hg verify'),
491 hint=_(b'run hg verify'),
503 )
492 )
504
493
505 for k, v in headers:
494 for k, v in headers:
506 request.add_header(pycompat.strurl(k), pycompat.strurl(v))
495 request.add_header(pycompat.strurl(k), pycompat.strurl(v))
507
496
508 try:
497 try:
509 if action == b'upload':
498 if action == b'upload':
510 request.data = lfsuploadfile(localstore.open(oid))
499 request.data = lfsuploadfile(self.ui, localstore.path(oid))
511 request.get_method = lambda: 'PUT'
500 request.get_method = lambda: 'PUT'
512 request.add_header('Content-Type', 'application/octet-stream')
501 request.add_header('Content-Type', 'application/octet-stream')
513 request.add_header('Content-Length', len(request.data))
502 request.add_header('Content-Length', request.data.length)
514
503
515 with contextlib.closing(self.urlopener.open(request)) as res:
504 with contextlib.closing(self.urlopener.open(request)) as res:
516 contentlength = res.info().get(b"content-length")
505 contentlength = res.info().get(b"content-length")
517 ui = self.ui # Shorten debug lines
506 ui = self.ui # Shorten debug lines
518 if self.ui.debugflag:
507 if self.ui.debugflag:
519 ui.debug(b'Status: %d\n' % res.status)
508 ui.debug(b'Status: %d\n' % res.status)
520 # lfs-test-server and hg serve return headers in different
509 # lfs-test-server and hg serve return headers in different
521 # order
510 # order
522 headers = pycompat.bytestr(res.info()).strip()
511 headers = pycompat.bytestr(res.info()).strip()
523 ui.debug(b'%s\n' % b'\n'.join(sorted(headers.splitlines())))
512 ui.debug(b'%s\n' % b'\n'.join(sorted(headers.splitlines())))
524
513
525 if action == b'download':
514 if action == b'download':
526 # If downloading blobs, store downloaded data to local
515 # If downloading blobs, store downloaded data to local
527 # blobstore
516 # blobstore
528 localstore.download(oid, res, contentlength)
517 localstore.download(oid, res, contentlength)
529 else:
518 else:
530 blocks = []
519 blocks = []
531 while True:
520 while True:
532 data = res.read(1048576)
521 data = res.read(1048576)
533 if not data:
522 if not data:
534 break
523 break
535 blocks.append(data)
524 blocks.append(data)
536
525
537 response = b"".join(blocks)
526 response = b"".join(blocks)
538 if response:
527 if response:
539 ui.debug(b'lfs %s response: %s' % (action, response))
528 ui.debug(b'lfs %s response: %s' % (action, response))
540 except util.urlerr.httperror as ex:
529 except util.urlerr.httperror as ex:
541 if self.ui.debugflag:
530 if self.ui.debugflag:
542 self.ui.debug(
531 self.ui.debug(
543 b'%s: %s\n' % (oid, ex.read())
532 b'%s: %s\n' % (oid, ex.read())
544 ) # XXX: also bytes?
533 ) # XXX: also bytes?
545 raise LfsRemoteError(
534 raise LfsRemoteError(
546 _(b'LFS HTTP error: %s (oid=%s, action=%s)')
535 _(b'LFS HTTP error: %s (oid=%s, action=%s)')
547 % (stringutil.forcebytestr(ex), oid, action)
536 % (stringutil.forcebytestr(ex), oid, action)
548 )
537 )
549 except util.urlerr.urlerror as ex:
538 except util.urlerr.urlerror as ex:
550 hint = _(b'attempted connection to %s') % pycompat.bytesurl(
539 hint = _(b'attempted connection to %s') % pycompat.bytesurl(
551 util.urllibcompat.getfullurl(request)
540 util.urllibcompat.getfullurl(request)
552 )
541 )
553 raise LfsRemoteError(
542 raise LfsRemoteError(
554 _(b'LFS error: %s') % _urlerrorreason(ex), hint=hint
543 _(b'LFS error: %s') % _urlerrorreason(ex), hint=hint
555 )
544 )
556 finally:
545 finally:
557 if request.data:
546 if request.data:
558 request.data.close()
547 request.data.close()
559
548
560 def _batch(self, pointers, localstore, action):
549 def _batch(self, pointers, localstore, action):
561 if action not in [b'upload', b'download']:
550 if action not in [b'upload', b'download']:
562 raise error.ProgrammingError(b'invalid Git-LFS action: %s' % action)
551 raise error.ProgrammingError(b'invalid Git-LFS action: %s' % action)
563
552
564 response = self._batchrequest(pointers, action)
553 response = self._batchrequest(pointers, action)
565 objects = self._extractobjects(response, pointers, action)
554 objects = self._extractobjects(response, pointers, action)
566 total = sum(x.get(b'size', 0) for x in objects)
555 total = sum(x.get(b'size', 0) for x in objects)
567 sizes = {}
556 sizes = {}
568 for obj in objects:
557 for obj in objects:
569 sizes[obj.get(b'oid')] = obj.get(b'size', 0)
558 sizes[obj.get(b'oid')] = obj.get(b'size', 0)
570 topic = {
559 topic = {
571 b'upload': _(b'lfs uploading'),
560 b'upload': _(b'lfs uploading'),
572 b'download': _(b'lfs downloading'),
561 b'download': _(b'lfs downloading'),
573 }[action]
562 }[action]
574 if len(objects) > 1:
563 if len(objects) > 1:
575 self.ui.note(
564 self.ui.note(
576 _(b'lfs: need to transfer %d objects (%s)\n')
565 _(b'lfs: need to transfer %d objects (%s)\n')
577 % (len(objects), util.bytecount(total))
566 % (len(objects), util.bytecount(total))
578 )
567 )
579
568
580 def transfer(chunk):
569 def transfer(chunk):
581 for obj in chunk:
570 for obj in chunk:
582 objsize = obj.get(b'size', 0)
571 objsize = obj.get(b'size', 0)
583 if self.ui.verbose:
572 if self.ui.verbose:
584 if action == b'download':
573 if action == b'download':
585 msg = _(b'lfs: downloading %s (%s)\n')
574 msg = _(b'lfs: downloading %s (%s)\n')
586 elif action == b'upload':
575 elif action == b'upload':
587 msg = _(b'lfs: uploading %s (%s)\n')
576 msg = _(b'lfs: uploading %s (%s)\n')
588 self.ui.note(
577 self.ui.note(
589 msg % (obj.get(b'oid'), util.bytecount(objsize))
578 msg % (obj.get(b'oid'), util.bytecount(objsize))
590 )
579 )
591 retry = self.retry
580 retry = self.retry
592 while True:
581 while True:
593 try:
582 try:
594 self._basictransfer(obj, action, localstore)
583 self._basictransfer(obj, action, localstore)
595 yield 1, obj.get(b'oid')
584 yield 1, obj.get(b'oid')
596 break
585 break
597 except socket.error as ex:
586 except socket.error as ex:
598 if retry > 0:
587 if retry > 0:
599 self.ui.note(
588 self.ui.note(
600 _(b'lfs: failed: %r (remaining retry %d)\n')
589 _(b'lfs: failed: %r (remaining retry %d)\n')
601 % (stringutil.forcebytestr(ex), retry)
590 % (stringutil.forcebytestr(ex), retry)
602 )
591 )
603 retry -= 1
592 retry -= 1
604 continue
593 continue
605 raise
594 raise
606
595
607 # Until https multiplexing gets sorted out
596 # Until https multiplexing gets sorted out
608 if self.ui.configbool(b'experimental', b'lfs.worker-enable'):
597 if self.ui.configbool(b'experimental', b'lfs.worker-enable'):
609 oids = worker.worker(
598 oids = worker.worker(
610 self.ui,
599 self.ui,
611 0.1,
600 0.1,
612 transfer,
601 transfer,
613 (),
602 (),
614 sorted(objects, key=lambda o: o.get(b'oid')),
603 sorted(objects, key=lambda o: o.get(b'oid')),
615 )
604 )
616 else:
605 else:
617 oids = transfer(sorted(objects, key=lambda o: o.get(b'oid')))
606 oids = transfer(sorted(objects, key=lambda o: o.get(b'oid')))
618
607
619 with self.ui.makeprogress(
608 with self.ui.makeprogress(
620 topic, unit=_(b"bytes"), total=total
609 topic, unit=_(b"bytes"), total=total
621 ) as progress:
610 ) as progress:
622 progress.update(0)
611 progress.update(0)
623 processed = 0
612 processed = 0
624 blobs = 0
613 blobs = 0
625 for _one, oid in oids:
614 for _one, oid in oids:
626 processed += sizes[oid]
615 processed += sizes[oid]
627 blobs += 1
616 blobs += 1
628 progress.update(processed)
617 progress.update(processed)
629 self.ui.note(_(b'lfs: processed: %s\n') % oid)
618 self.ui.note(_(b'lfs: processed: %s\n') % oid)
630
619
631 if blobs > 0:
620 if blobs > 0:
632 if action == b'upload':
621 if action == b'upload':
633 self.ui.status(
622 self.ui.status(
634 _(b'lfs: uploaded %d files (%s)\n')
623 _(b'lfs: uploaded %d files (%s)\n')
635 % (blobs, util.bytecount(processed))
624 % (blobs, util.bytecount(processed))
636 )
625 )
637 elif action == b'download':
626 elif action == b'download':
638 self.ui.status(
627 self.ui.status(
639 _(b'lfs: downloaded %d files (%s)\n')
628 _(b'lfs: downloaded %d files (%s)\n')
640 % (blobs, util.bytecount(processed))
629 % (blobs, util.bytecount(processed))
641 )
630 )
642
631
643 def __del__(self):
632 def __del__(self):
644 # copied from mercurial/httppeer.py
633 # copied from mercurial/httppeer.py
645 urlopener = getattr(self, 'urlopener', None)
634 urlopener = getattr(self, 'urlopener', None)
646 if urlopener:
635 if urlopener:
647 for h in urlopener.handlers:
636 for h in urlopener.handlers:
648 h.close()
637 h.close()
649 getattr(h, "close_all", lambda: None)()
638 getattr(h, "close_all", lambda: None)()
650
639
651
640
652 class _dummyremote(object):
641 class _dummyremote(object):
653 """Dummy store storing blobs to temp directory."""
642 """Dummy store storing blobs to temp directory."""
654
643
655 def __init__(self, repo, url):
644 def __init__(self, repo, url):
656 fullpath = repo.vfs.join(b'lfs', url.path)
645 fullpath = repo.vfs.join(b'lfs', url.path)
657 self.vfs = lfsvfs(fullpath)
646 self.vfs = lfsvfs(fullpath)
658
647
659 def writebatch(self, pointers, fromstore):
648 def writebatch(self, pointers, fromstore):
660 for p in _deduplicate(pointers):
649 for p in _deduplicate(pointers):
661 content = fromstore.read(p.oid(), verify=True)
650 content = fromstore.read(p.oid(), verify=True)
662 with self.vfs(p.oid(), b'wb', atomictemp=True) as fp:
651 with self.vfs(p.oid(), b'wb', atomictemp=True) as fp:
663 fp.write(content)
652 fp.write(content)
664
653
665 def readbatch(self, pointers, tostore):
654 def readbatch(self, pointers, tostore):
666 for p in _deduplicate(pointers):
655 for p in _deduplicate(pointers):
667 with self.vfs(p.oid(), b'rb') as fp:
656 with self.vfs(p.oid(), b'rb') as fp:
668 tostore.download(p.oid(), fp, None)
657 tostore.download(p.oid(), fp, None)
669
658
670
659
671 class _nullremote(object):
660 class _nullremote(object):
672 """Null store storing blobs to /dev/null."""
661 """Null store storing blobs to /dev/null."""
673
662
674 def __init__(self, repo, url):
663 def __init__(self, repo, url):
675 pass
664 pass
676
665
677 def writebatch(self, pointers, fromstore):
666 def writebatch(self, pointers, fromstore):
678 pass
667 pass
679
668
680 def readbatch(self, pointers, tostore):
669 def readbatch(self, pointers, tostore):
681 pass
670 pass
682
671
683
672
684 class _promptremote(object):
673 class _promptremote(object):
685 """Prompt user to set lfs.url when accessed."""
674 """Prompt user to set lfs.url when accessed."""
686
675
687 def __init__(self, repo, url):
676 def __init__(self, repo, url):
688 pass
677 pass
689
678
690 def writebatch(self, pointers, fromstore, ui=None):
679 def writebatch(self, pointers, fromstore, ui=None):
691 self._prompt()
680 self._prompt()
692
681
693 def readbatch(self, pointers, tostore, ui=None):
682 def readbatch(self, pointers, tostore, ui=None):
694 self._prompt()
683 self._prompt()
695
684
696 def _prompt(self):
685 def _prompt(self):
697 raise error.Abort(_(b'lfs.url needs to be configured'))
686 raise error.Abort(_(b'lfs.url needs to be configured'))
698
687
699
688
700 _storemap = {
689 _storemap = {
701 b'https': _gitlfsremote,
690 b'https': _gitlfsremote,
702 b'http': _gitlfsremote,
691 b'http': _gitlfsremote,
703 b'file': _dummyremote,
692 b'file': _dummyremote,
704 b'null': _nullremote,
693 b'null': _nullremote,
705 None: _promptremote,
694 None: _promptremote,
706 }
695 }
707
696
708
697
709 def _deduplicate(pointers):
698 def _deduplicate(pointers):
710 """Remove any duplicate oids that exist in the list"""
699 """Remove any duplicate oids that exist in the list"""
711 reduced = util.sortdict()
700 reduced = util.sortdict()
712 for p in pointers:
701 for p in pointers:
713 reduced[p.oid()] = p
702 reduced[p.oid()] = p
714 return reduced.values()
703 return reduced.values()
715
704
716
705
717 def _verify(oid, content):
706 def _verify(oid, content):
718 realoid = node.hex(hashlib.sha256(content).digest())
707 realoid = node.hex(hashlib.sha256(content).digest())
719 if realoid != oid:
708 if realoid != oid:
720 raise LfsCorruptionError(
709 raise LfsCorruptionError(
721 _(b'detected corrupt lfs object: %s') % oid,
710 _(b'detected corrupt lfs object: %s') % oid,
722 hint=_(b'run hg verify'),
711 hint=_(b'run hg verify'),
723 )
712 )
724
713
725
714
726 def remote(repo, remote=None):
715 def remote(repo, remote=None):
727 """remotestore factory. return a store in _storemap depending on config
716 """remotestore factory. return a store in _storemap depending on config
728
717
729 If ``lfs.url`` is specified, use that remote endpoint. Otherwise, try to
718 If ``lfs.url`` is specified, use that remote endpoint. Otherwise, try to
730 infer the endpoint, based on the remote repository using the same path
719 infer the endpoint, based on the remote repository using the same path
731 adjustments as git. As an extension, 'http' is supported as well so that
720 adjustments as git. As an extension, 'http' is supported as well so that
732 ``hg serve`` works out of the box.
721 ``hg serve`` works out of the box.
733
722
734 https://github.com/git-lfs/git-lfs/blob/master/docs/api/server-discovery.md
723 https://github.com/git-lfs/git-lfs/blob/master/docs/api/server-discovery.md
735 """
724 """
736 lfsurl = repo.ui.config(b'lfs', b'url')
725 lfsurl = repo.ui.config(b'lfs', b'url')
737 url = util.url(lfsurl or b'')
726 url = util.url(lfsurl or b'')
738 if lfsurl is None:
727 if lfsurl is None:
739 if remote:
728 if remote:
740 path = remote
729 path = remote
741 elif util.safehasattr(repo, b'_subtoppath'):
730 elif util.safehasattr(repo, b'_subtoppath'):
742 # The pull command sets this during the optional update phase, which
731 # The pull command sets this during the optional update phase, which
743 # tells exactly where the pull originated, whether 'paths.default'
732 # tells exactly where the pull originated, whether 'paths.default'
744 # or explicit.
733 # or explicit.
745 path = repo._subtoppath
734 path = repo._subtoppath
746 else:
735 else:
747 # TODO: investigate 'paths.remote:lfsurl' style path customization,
736 # TODO: investigate 'paths.remote:lfsurl' style path customization,
748 # and fall back to inferring from 'paths.remote' if unspecified.
737 # and fall back to inferring from 'paths.remote' if unspecified.
749 path = repo.ui.config(b'paths', b'default') or b''
738 path = repo.ui.config(b'paths', b'default') or b''
750
739
751 defaulturl = util.url(path)
740 defaulturl = util.url(path)
752
741
753 # TODO: support local paths as well.
742 # TODO: support local paths as well.
754 # TODO: consider the ssh -> https transformation that git applies
743 # TODO: consider the ssh -> https transformation that git applies
755 if defaulturl.scheme in (b'http', b'https'):
744 if defaulturl.scheme in (b'http', b'https'):
756 if defaulturl.path and defaulturl.path[:-1] != b'/':
745 if defaulturl.path and defaulturl.path[:-1] != b'/':
757 defaulturl.path += b'/'
746 defaulturl.path += b'/'
758 defaulturl.path = (defaulturl.path or b'') + b'.git/info/lfs'
747 defaulturl.path = (defaulturl.path or b'') + b'.git/info/lfs'
759
748
760 url = util.url(bytes(defaulturl))
749 url = util.url(bytes(defaulturl))
761 repo.ui.note(_(b'lfs: assuming remote store: %s\n') % url)
750 repo.ui.note(_(b'lfs: assuming remote store: %s\n') % url)
762
751
763 scheme = url.scheme
752 scheme = url.scheme
764 if scheme not in _storemap:
753 if scheme not in _storemap:
765 raise error.Abort(_(b'lfs: unknown url scheme: %s') % scheme)
754 raise error.Abort(_(b'lfs: unknown url scheme: %s') % scheme)
766 return _storemap[scheme](repo, url)
755 return _storemap[scheme](repo, url)
767
756
768
757
769 class LfsRemoteError(error.StorageError):
758 class LfsRemoteError(error.StorageError):
770 pass
759 pass
771
760
772
761
773 class LfsCorruptionError(error.Abort):
762 class LfsCorruptionError(error.Abort):
774 """Raised when a corrupt blob is detected, aborting an operation
763 """Raised when a corrupt blob is detected, aborting an operation
775
764
776 It exists to allow specialized handling on the server side."""
765 It exists to allow specialized handling on the server side."""
General Comments 0
You need to be logged in to leave comments. Login now