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