##// END OF EJS Templates
errors: raise InputError from revpair() iff revset provided by the user...
errors: raise InputError from revpair() iff revset provided by the user Same reasoning as for `revrange()` in an earlier patch. Differential Revision: https://phab.mercurial-scm.org/D11561

File last commit:

r47669:ffd3e823 default
r48929:b74e1286 default
Show More
blobstore.py
770 lines | 27.4 KiB | text/x-python | PythonLexer
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 # blobstore.py - local and remote (speaking Git-LFS protocol) blob storages
#
# Copyright 2017 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import absolute_import
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 import contextlib
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 import errno
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 import hashlib
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 import json
import os
import re
Matt Harbison
lfs: narrow the exceptions that trigger a transfer retry...
r35491 import socket
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: quiesce check-module-import warnings...
r35098 from mercurial.i18n import _
Gregory Szorc
py3: manually import getattr where it is needed...
r43359 from mercurial.pycompat import getattr
Joerg Sonnenberger
node: import symbols explicitly...
r46729 from mercurial.node import hex
Matt Harbison
lfs: quiesce check-module-import warnings...
r35098
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 from mercurial import (
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 encoding,
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 error,
Matt Harbison
lfs: fix the stall and corruption issue when concurrently uploading blobs...
r44746 httpconnection as httpconnectionmod,
Matt Harbison
lfs: override walk() in lfsvfs...
r35363 pathutil,
Augie Fackler
lfs: add some bytestring wrappers in blobstore.py...
r36619 pycompat,
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 url as urlmod,
util,
vfs as vfsmod,
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 worker,
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 )
urlutil: extract `url` related code from `util` into the new module...
r47669 from mercurial.utils import (
stringutil,
urlutil,
)
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281 from ..largefiles import lfutil
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 # 64 bytes for SHA256
Pulkit Goyal
py3: make sure regexes are bytes...
r36473 _lfsre = re.compile(br'\A[a-f0-9]{64}\Z')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 class lfsvfs(vfsmod.vfs):
def join(self, path):
"""split the path at first two characters, like: XX/XXXXX..."""
if not _lfsre.match(path):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise error.ProgrammingError(b'unexpected lfs path: %s' % path)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 return super(lfsvfs, self).join(path[0:2], path[2:])
Matt Harbison
lfs: override walk() in lfsvfs...
r35363 def walk(self, path=None, onerror=None):
Matt Harbison
lfs: correct the directory list value returned by lfsvfs.walk()...
r35397 """Yield (dirpath, [], oids) tuple for blobs under path
Matt Harbison
lfs: override walk() in lfsvfs...
r35363
Oids only exist in the root of this vfs, so dirpath is always ''.
"""
root = os.path.normpath(self.base)
# when dirpath == root, dirpath[prefixlen:] becomes empty
# because len(dirpath) < prefixlen.
prefixlen = len(pathutil.normasprefix(root))
oids = []
Augie Fackler
formatting: blacken the codebase...
r43346 for dirpath, dirs, files in os.walk(
self.reljoin(self.base, path or b''), onerror=onerror
):
Matt Harbison
lfs: override walk() in lfsvfs...
r35363 dirpath = dirpath[prefixlen:]
# Silently skip unexpected files and directories
if len(dirpath) == 2:
Augie Fackler
formatting: blacken the codebase...
r43346 oids.extend(
[dirpath + f for f in files if _lfsre.match(dirpath + f)]
)
Matt Harbison
lfs: override walk() in lfsvfs...
r35363
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 yield (b'', [], oids)
Matt Harbison
lfs: override walk() in lfsvfs...
r35363
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 class nullvfs(lfsvfs):
def __init__(self):
pass
def exists(self, oid):
return False
def read(self, oid):
# store.read() calls into here if the blob doesn't exist in its
# self.vfs. Raise the same error as a normal vfs when asked to read a
# file that doesn't exist. The only difference is the full file path
# isn't available in the error.
Augie Fackler
formatting: blacken the codebase...
r43346 raise IOError(
errno.ENOENT,
pycompat.sysstr(b'%s: No such file or directory' % oid),
)
Matt Harbison
lfs: add the ability to disable the usercache...
r37535
def walk(self, path=None, onerror=None):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 return (b'', [], [])
Matt Harbison
lfs: add the ability to disable the usercache...
r37535
def write(self, oid, data):
pass
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: fix the stall and corruption issue when concurrently uploading blobs...
r44746 class lfsuploadfile(httpconnectionmod.httpsendfile):
Augie Fackler
formating: upgrade to black 20.8b1...
r46554 """a file-like object that supports keepalive."""
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: fix the stall and corruption issue when concurrently uploading blobs...
r44746 def __init__(self, ui, filename):
super(lfsuploadfile, self).__init__(ui, filename, b'rb')
self.read = self._data.read
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: fix the stall and corruption issue when concurrently uploading blobs...
r44746 def _makeprogress(self):
return None # progress is handled by the worker client
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 class local(object):
"""Local blobstore for large file contents.
This blobstore is used both as a cache and as a staging area for large blobs
to be uploaded to the remote blobstore.
"""
def __init__(self, repo):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 fullpath = repo.svfs.join(b'lfs/objects')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 self.vfs = lfsvfs(fullpath)
Matt Harbison
lfs: special case the null:// usercache instead of treating it as a url...
r37580
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if repo.ui.configbool(b'experimental', b'lfs.disableusercache'):
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 self.cachevfs = nullvfs()
else:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 usercache = lfutil._usercachedir(repo.ui, b'lfs')
Matt Harbison
lfs: special case the null:// usercache instead of treating it as a url...
r37580 self.cachevfs = lfsvfs(usercache)
Matt Harbison
lfs: add note messages indicating what store holds the lfs blob...
r35489 self.ui = repo.ui
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: add a local store method for opening a blob...
r35543 def open(self, oid):
"""Open a read-only file descriptor to the named blob, in either the
usercache or the local store."""
Matt Harbison
lfs: use str for the open() mode when opening a blob for py3...
r44776 return open(self.path(oid), 'rb')
Matt Harbison
lfs: add a method to the local blobstore to convert OIDs to file paths...
r44745
def path(self, oid):
"""Build the path for the given blob ``oid``.
If the blob exists locally, the path may point to either the usercache
or the local store. If it doesn't, it will point to the local store.
This is meant for situations where existing code that isn't LFS aware
needs to open a blob. Generally, prefer the ``open`` method on this
class.
"""
Matt Harbison
lfs: add a comment to describe subtle local blobstore open() behavior
r35555 # The usercache is the most likely place to hold the file. Commit will
# write to both it and the local store, as will anything that downloads
# the blobs. However, things like clone without an update won't
# populate the local store. For an init + push of a local clone,
# the usercache is the only place it _could_ be. If not present, the
# missing file msg here will indicate the local repo, not the usercache.
Matt Harbison
lfs: add a local store method for opening a blob...
r35543 if self.cachevfs.exists(oid):
Matt Harbison
lfs: add a method to the local blobstore to convert OIDs to file paths...
r44745 return self.cachevfs.join(oid)
Matt Harbison
lfs: add a local store method for opening a blob...
r35543
Matt Harbison
lfs: add a method to the local blobstore to convert OIDs to file paths...
r44745 return self.vfs.join(oid)
Matt Harbison
lfs: add a local store method for opening a blob...
r35543
Matt Harbison
lfs: check content length after downloading content...
r44544 def download(self, oid, src, content_length):
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565 """Read the blob from the remote source in chunks, verify the content,
and write to this local blobstore."""
sha256 = hashlib.sha256()
Matt Harbison
lfs: check content length after downloading content...
r44544 size = 0
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 with self.vfs(oid, b'wb', atomictemp=True) as fp:
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565 for chunk in util.filechunkiter(src, size=1048576):
fp.write(chunk)
sha256.update(chunk)
Matt Harbison
lfs: check content length after downloading content...
r44544 size += len(chunk)
# If the server advertised a length longer than what we actually
# received, then we should expect that the server crashed while
# producing the response (but the server has no way of telling us
# that), and we really don't need to try to write the response to
# the localstore, because it's not going to match the expected.
if content_length is not None and int(content_length) != size:
msg = (
b"Response length (%s) does not match Content-Length "
b"header (%d): likely server-side crash"
)
raise LfsRemoteError(_(msg) % (size, int(content_length)))
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565
Joerg Sonnenberger
node: import symbols explicitly...
r46729 realoid = hex(sha256.digest())
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565 if realoid != oid:
Augie Fackler
formatting: blacken the codebase...
r43346 raise LfsCorruptionError(
_(b'corrupt remote lfs object: %s') % oid
)
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 self._linktousercache(oid)
Matt Harbison
lfs: introduce a localstore method for downloading from remote stores...
r35565
Matt Harbison
lfs: remove the verification option when writing to the local store...
r35567 def write(self, oid, data):
"""Write blob to local blobstore.
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492
Matt Harbison
lfs: remove the verification option when writing to the local store...
r35567 This should only be called from the filelog during a commit or similar.
As such, there is no need to verify the data. Imports from a remote
store must use ``download()`` instead."""
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 with self.vfs(oid, b'wb', atomictemp=True) as fp:
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 fp.write(data)
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 self._linktousercache(oid)
Matt Harbison
lfs: ensure the blob is linked to the remote store on skipped uploads...
r39491 def linkfromusercache(self, oid):
"""Link blobs found in the user cache into this store.
The server module needs to do this when it lets the client know not to
upload the blob, to ensure it is always available in this store.
Normally this is done implicitly when the client reads or writes the
blob, but that doesn't happen when the server tells the client that it
already has the blob.
"""
Augie Fackler
formatting: blacken the codebase...
r43346 if not isinstance(self.cachevfs, nullvfs) and not self.vfs.exists(oid):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: found %s in the usercache\n') % oid)
Matt Harbison
lfs: ensure the blob is linked to the remote store on skipped uploads...
r39491 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
Matt Harbison
lfs: add the ability to disable the usercache...
r37535 def _linktousercache(self, oid):
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281 # XXX: should we verify the content of the cache, and hardlink back to
# the local store on success, but truncate, write and link on failure?
Augie Fackler
formatting: blacken the codebase...
r43346 if not self.cachevfs.exists(oid) and not isinstance(
self.cachevfs, nullvfs
):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: adding %s to the usercache\n') % oid)
Matt Harbison
lfs: remove the verification option when writing to the local store...
r35567 lfutil.link(self.vfs.join(oid), self.cachevfs.join(oid))
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 def read(self, oid, verify=True):
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 """Read blob from local blobstore."""
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281 if not self.vfs.exists(oid):
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 blob = self._read(self.cachevfs, oid, verify)
Matt Harbison
lfs: only hardlink between the usercache and local store if the blob verifies...
r35493
# Even if revlog will verify the content, it needs to be verified
# now before making the hardlink to avoid propagating corrupt blobs.
# Don't abort if corruption is detected, because `hg verify` will
# give more useful info about the corruption- simply don't add the
# hardlink.
Joerg Sonnenberger
node: import symbols explicitly...
r46729 if verify or hex(hashlib.sha256(blob).digest()) == oid:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: found %s in the usercache\n') % oid)
Matt Harbison
lfs: only hardlink between the usercache and local store if the blob verifies...
r35493 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
Matt Harbison
lfs: add note messages indicating what store holds the lfs blob...
r35489 else:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: found %s in the local lfs store\n') % oid)
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 blob = self._read(self.vfs, oid, verify)
return blob
def _read(self, vfs, oid, verify):
"""Read blob (after verifying) from the given store"""
blob = vfs.read(oid)
if verify:
_verify(oid, blob)
return blob
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: add a blob verification method to the local store...
r37163 def verify(self, oid):
"""Indicate whether or not the hash of the underlying file matches its
name."""
sha256 = hashlib.sha256()
with self.open(oid) as fp:
for chunk in util.filechunkiter(fp, size=1048576):
sha256.update(chunk)
Joerg Sonnenberger
node: import symbols explicitly...
r46729 return oid == hex(sha256.digest())
Matt Harbison
lfs: add a blob verification method to the local store...
r37163
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 def has(self, oid):
"""Returns True if the local blobstore contains the requested blob,
False otherwise."""
Matt Harbison
lfs: introduce a user level cache for lfs files...
r35281 return self.cachevfs.exists(oid) or self.vfs.exists(oid)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 def _urlerrorreason(urlerror):
Augie Fackler
formating: upgrade to black 20.8b1...
r46554 """Create a friendly message for the given URLError to be used in an
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 LfsRemoteError message.
Augie Fackler
formating: upgrade to black 20.8b1...
r46554 """
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 inst = urlerror
if isinstance(urlerror.reason, Exception):
inst = urlerror.reason
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 if util.safehasattr(inst, b'reason'):
Augie Fackler
formatting: blacken the codebase...
r43346 try: # usually it is in the form (errno, strerror)
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 reason = inst.reason.args[1]
except (AttributeError, IndexError):
# it might be anything, for example a string
reason = inst.reason
if isinstance(reason, pycompat.unicode):
# SSLError of Python 2.7.9 contains a unicode
reason = encoding.unitolocal(reason)
return reason
elif getattr(inst, "strerror", None):
return encoding.strtolocal(inst.strerror)
else:
return stringutil.forcebytestr(urlerror)
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: disable all authentication except Basic for HTTP(S) connections...
r41756 class lfsauthhandler(util.urlreq.basehandler):
handler_order = 480 # Before HTTPDigestAuthHandler (== 490)
def http_error_401(self, req, fp, code, msg, headers):
"""Enforces that any authentication performed is HTTP Basic
Authentication. No authentication is also acceptable.
"""
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 authreq = headers.get('www-authenticate', None)
Matt Harbison
lfs: disable all authentication except Basic for HTTP(S) connections...
r41756 if authreq:
scheme = authreq.split()[0]
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 if scheme.lower() != 'basic':
Matt Harbison
lfs: disable all authentication except Basic for HTTP(S) connections...
r41756 msg = _(b'the server must support Basic Authentication')
Augie Fackler
formatting: blacken the codebase...
r43346 raise util.urlerr.httperror(
req.get_full_url(),
code,
encoding.strfromlocal(msg),
headers,
fp,
)
Matt Harbison
lfs: disable all authentication except Basic for HTTP(S) connections...
r41756 return None
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 class _gitlfsremote(object):
def __init__(self, repo, url):
ui = repo.ui
self.ui = ui
baseurl, authinfo = url.authinfo()
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.baseurl = baseurl.rstrip(b'/')
useragent = repo.ui.config(b'experimental', b'lfs.user-agent')
Matt Harbison
lfs: add an experimental config to override User-Agent for the blob transfer...
r35456 if not useragent:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 useragent = b'git-lfs/2.3.4 (Mercurial %s)' % util.version()
Matt Harbison
lfs: add git to the User-Agent header for blob transfers...
r35455 self.urlopener = urlmod.opener(ui, authinfo, useragent)
Matt Harbison
lfs: disable all authentication except Basic for HTTP(S) connections...
r41756 self.urlopener.add_handler(lfsauthhandler())
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.retry = ui.configint(b'lfs', b'retry')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def writebatch(self, pointers, fromstore):
"""Batch upload from local to remote blobstore."""
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self._batch(_deduplicate(pointers), fromstore, b'upload')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def readbatch(self, pointers, tostore):
"""Batch download from remote to local blostore."""
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self._batch(_deduplicate(pointers), tostore, b'download')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def _batchrequest(self, pointers, action):
"""Get metadata about objects pointed by pointers for given action
Return decoded JSON object like {'objects': [{'oid': '', 'size': 1}]}
See https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
"""
Augie Fackler
formatting: blacken the codebase...
r43346 objects = [
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 {'oid': pycompat.strurl(p.oid()), 'size': p.size()}
Augie Fackler
formatting: blacken the codebase...
r43346 for p in pointers
]
requestdata = pycompat.bytesurl(
json.dumps(
Augie Fackler
formating: upgrade to black 20.8b1...
r46554 {
'objects': objects,
'operation': pycompat.strurl(action),
}
Augie Fackler
formatting: blacken the codebase...
r43346 )
)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 url = b'%s/objects/batch' % self.baseurl
batchreq = util.urlreq.request(pycompat.strurl(url), data=requestdata)
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 batchreq.add_header('Accept', 'application/vnd.git-lfs+json')
batchreq.add_header('Content-Type', 'application/vnd.git-lfs+json')
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 try:
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 with contextlib.closing(self.urlopener.open(batchreq)) as rsp:
rawjson = rsp.read()
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 except util.urlerr.httperror as ex:
Matt Harbison
lfs: improve the hints for common errors in the Batch API...
r40696 hints = {
Augie Fackler
formatting: blacken the codebase...
r43346 400: _(
b'check that lfs serving is enabled on %s and "%s" is '
b'supported'
)
% (self.baseurl, action),
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 404: _(b'the "lfs.url" config may be used to override %s')
Augie Fackler
formatting: blacken the codebase...
r43346 % self.baseurl,
Matt Harbison
lfs: improve the hints for common errors in the Batch API...
r40696 }
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 hint = hints.get(ex.code, _(b'api=%s, action=%s') % (url, action))
raise LfsRemoteError(
_(b'LFS HTTP error: %s') % stringutil.forcebytestr(ex),
Augie Fackler
formatting: blacken the codebase...
r43346 hint=hint,
)
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 except util.urlerr.urlerror as ex:
Augie Fackler
formatting: blacken the codebase...
r43346 hint = (
_(b'the "lfs.url" config may be used to override %s')
% self.baseurl
)
raise LfsRemoteError(
_(b'LFS error: %s') % _urlerrorreason(ex), hint=hint
)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 try:
Gregory Szorc
py3: define and use json.loads polyfill...
r43697 response = pycompat.json_loads(rawjson)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 except ValueError:
Augie Fackler
formatting: blacken the codebase...
r43346 raise LfsRemoteError(
_(b'LFS server returns invalid JSON: %s')
% rawjson.encode("utf-8")
)
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944
if self.ui.debugflag:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.debug(b'Status: %d\n' % rsp.status)
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944 # lfs-test-server and hg serve return headers in different order
Matt Harbison
lfs: strip the response headers from the Batch API before printing...
r41476 headers = pycompat.bytestr(rsp.info()).strip()
Augie Fackler
formatting: blacken the codebase...
r43346 self.ui.debug(b'%s\n' % b'\n'.join(sorted(headers.splitlines())))
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 if 'objects' in response:
response['objects'] = sorted(
response['objects'], key=lambda p: p['oid']
Augie Fackler
formatting: blacken the codebase...
r43346 )
self.ui.debug(
b'%s\n'
% pycompat.bytesurl(
json.dumps(
response,
indent=2,
Augie Fackler
cleanup: remove pointless r-prefixes on single-quoted strings...
r43906 separators=('', ': '),
Augie Fackler
formatting: blacken the codebase...
r43346 sort_keys=True,
)
)
)
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 def encodestr(x):
if isinstance(x, pycompat.unicode):
Gregory Szorc
py3: stop normalizing .encode()/.decode() arguments to unicode...
r43361 return x.encode('utf-8')
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 return x
return pycompat.rapply(encodestr, response)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Jun Wu
lfs: remove internal url in test...
r35684 def _checkforservererror(self, pointers, responses, action):
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 """Scans errors from objects
Matt Harbison
lfs: correct documentation typo
r35712 Raises LfsRemoteError if any objects have an error"""
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 for response in responses:
Jun Wu
lfs: remove internal url in test...
r35684 # The server should return 404 when objects cannot be found. Some
# server implementation (ex. lfs-test-server) does not set "error"
# but just removes "download" from "actions". Treat that case
# as the same as 404 error.
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if b'error' not in response:
Augie Fackler
formatting: blacken the codebase...
r43346 if action == b'download' and action not in response.get(
b'actions', []
):
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 code = 404
Matt Harbison
lfs: raise an error if the server sends an unsolicited oid...
r35713 else:
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 continue
else:
# An error dict without a code doesn't make much sense, so
# treat as a server error.
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 code = response.get(b'error').get(b'code', 500)
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259
ptrmap = {p.oid(): p for p in pointers}
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 p = ptrmap.get(response[b'oid'], None)
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 if p:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 filename = getattr(p, 'filename', b'unknown')
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 errors = {
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 404: b'The object does not exist',
410: b'The object was removed by the owner',
422: b'Validation error',
500: b'Internal server error',
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 }
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 msg = errors.get(code, b'status code %d' % code)
Augie Fackler
formatting: blacken the codebase...
r43346 raise LfsRemoteError(
_(b'LFS server error for "%s": %s') % (filename, msg)
)
Matt Harbison
lfs: improve the client message when the server signals an object error...
r37259 else:
raise LfsRemoteError(
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 _(b'LFS server error. Unsolicited response for oid %s')
Augie Fackler
formatting: blacken the codebase...
r43346 % response[b'oid']
)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def _extractobjects(self, response, pointers, action):
"""extract objects from response of the batch API
response: parsed JSON object returned by batch API
return response['objects'] filtered by action
raise if any object has an error
"""
# Scan errors from objects - fail early
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 objects = response.get(b'objects', [])
Jun Wu
lfs: remove internal url in test...
r35684 self._checkforservererror(pointers, objects, action)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
# Filter objects with given action. Practically, this skips uploading
# objects which exist in the server.
Augie Fackler
formatting: blacken the codebase...
r43346 filteredobjects = [
o for o in objects if action in o.get(b'actions', [])
]
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
return filteredobjects
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 def _basictransfer(self, obj, action, localstore):
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 """Download or upload a single object using basic transfer protocol
obj: dict, an object description returned by batch API
action: string, one of ['upload', 'download']
localstore: blobstore.local
See https://github.com/git-lfs/git-lfs/blob/master/docs/api/\
basic-transfers.md
"""
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 oid = obj[b'oid']
href = obj[b'actions'][action].get(b'href')
headers = obj[b'actions'][action].get(b'header', {}).items()
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 request = util.urlreq.request(pycompat.strurl(href))
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action == b'upload':
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 # If uploading blobs, read data from local blobstore.
Matt Harbison
lfs: drop a duplicate blob verification method
r37234 if not localstore.verify(oid):
Augie Fackler
formatting: blacken the codebase...
r43346 raise error.Abort(
_(b'detected corrupt lfs object: %s') % oid,
hint=_(b'run hg verify'),
)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
for k, v in headers:
Matt Harbison
py3: byteify the decoded JSON responses upon receipt in the LFS blobstore...
r41474 request.add_header(pycompat.strurl(k), pycompat.strurl(v))
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
try:
Matt Harbison
lfs: move the initialization of the upload request into the try block...
r44599 if action == b'upload':
Matt Harbison
lfs: fix the stall and corruption issue when concurrently uploading blobs...
r44746 request.data = lfsuploadfile(self.ui, localstore.path(oid))
Matt Harbison
lfs: move the initialization of the upload request into the try block...
r44599 request.get_method = lambda: 'PUT'
request.add_header('Content-Type', 'application/octet-stream')
Matt Harbison
lfs: fix the stall and corruption issue when concurrently uploading blobs...
r44746 request.add_header('Content-Length', request.data.length)
Matt Harbison
lfs: move the initialization of the upload request into the try block...
r44599
Matt Harbison
lfs: rename a variable to clarify its use...
r44543 with contextlib.closing(self.urlopener.open(request)) as res:
Matt Harbison
lfs: check content length after downloading content...
r44544 contentlength = res.info().get(b"content-length")
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 ui = self.ui # Shorten debug lines
if self.ui.debugflag:
Matt Harbison
lfs: rename a variable to clarify its use...
r44543 ui.debug(b'Status: %d\n' % res.status)
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 # lfs-test-server and hg serve return headers in different
# order
Matt Harbison
lfs: rename a variable to clarify its use...
r44543 headers = pycompat.bytestr(res.info()).strip()
Augie Fackler
formatting: blacken the codebase...
r43346 ui.debug(b'%s\n' % b'\n'.join(sorted(headers.splitlines())))
Matt Harbison
lfs: debug print HTTP headers and JSON payload received from the server...
r36944
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action == b'download':
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 # If downloading blobs, store downloaded data to local
# blobstore
Matt Harbison
lfs: check content length after downloading content...
r44544 localstore.download(oid, res, contentlength)
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 else:
Matt Harbison
lfs: avoid quadratic performance in processing server responses...
r44545 blocks = []
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 while True:
Matt Harbison
lfs: rename a variable to clarify its use...
r44543 data = res.read(1048576)
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 if not data:
break
Matt Harbison
lfs: avoid quadratic performance in processing server responses...
r44545 blocks.append(data)
response = b"".join(blocks)
Matt Harbison
lfs: ensure that the return of urlopener.open() is closed...
r40701 if response:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 ui.debug(b'lfs %s response: %s' % (action, response))
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 except util.urlerr.httperror as ex:
Matt Harbison
lfs: dump the full response on httperror in debug mode...
r35752 if self.ui.debugflag:
Augie Fackler
formatting: blacken the codebase...
r43346 self.ui.debug(
b'%s: %s\n' % (oid, ex.read())
) # XXX: also bytes?
raise LfsRemoteError(
_(b'LFS HTTP error: %s (oid=%s, action=%s)')
% (stringutil.forcebytestr(ex), oid, action)
)
Matt Harbison
lfs: handle URLErrors to add additional information...
r40697 except util.urlerr.urlerror as ex:
Augie Fackler
formatting: blacken the codebase...
r43346 hint = _(b'attempted connection to %s') % pycompat.bytesurl(
util.urllibcompat.getfullurl(request)
)
raise LfsRemoteError(
_(b'LFS error: %s') % _urlerrorreason(ex), hint=hint
)
Matt Harbison
lfs: explicitly close the file handle for the blob being uploaded...
r44597 finally:
if request.data:
request.data.close()
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
def _batch(self, pointers, localstore, action):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action not in [b'upload', b'download']:
raise error.ProgrammingError(b'invalid Git-LFS action: %s' % action)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
response = self._batchrequest(pointers, action)
objects = self._extractobjects(response, pointers, action)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 total = sum(x.get(b'size', 0) for x in objects)
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 sizes = {}
for obj in objects:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 sizes[obj.get(b'oid')] = obj.get(b'size', 0)
Augie Fackler
formatting: blacken the codebase...
r43346 topic = {
b'upload': _(b'lfs uploading'),
b'download': _(b'lfs downloading'),
}[action]
Matt Harbison
lfs: use ui.note() and ui.debug() instead of ui.write() and their flags...
r35494 if len(objects) > 1:
Augie Fackler
formatting: blacken the codebase...
r43346 self.ui.note(
_(b'lfs: need to transfer %d objects (%s)\n')
% (len(objects), util.bytecount(total))
)
Matt Harbison
lfs: use a context manager to control the progress bar lifetime
r39426
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 def transfer(chunk):
for obj in chunk:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 objsize = obj.get(b'size', 0)
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 if self.ui.verbose:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action == b'download':
msg = _(b'lfs: downloading %s (%s)\n')
elif action == b'upload':
msg = _(b'lfs: uploading %s (%s)\n')
Augie Fackler
formatting: blacken the codebase...
r43346 self.ui.note(
msg % (obj.get(b'oid'), util.bytecount(objsize))
)
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 retry = self.retry
while True:
try:
self._basictransfer(obj, action, localstore)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 yield 1, obj.get(b'oid')
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 break
Matt Harbison
lfs: narrow the exceptions that trigger a transfer retry...
r35491 except socket.error as ex:
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 if retry > 0:
Matt Harbison
lfs: use ui.note() and ui.debug() instead of ui.write() and their flags...
r35494 self.ui.note(
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 _(b'lfs: failed: %r (remaining retry %d)\n')
Augie Fackler
formatting: blacken the codebase...
r43346 % (stringutil.forcebytestr(ex), retry)
)
Wojciech Lis
lfs: using workers in lfs prefetch...
r35449 retry -= 1
continue
raise
Matt Harbison
lfs: default to not using workers for upload/download...
r35750 # Until https multiplexing gets sorted out
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if self.ui.configbool(b'experimental', b'lfs.worker-enable'):
Augie Fackler
formatting: blacken the codebase...
r43346 oids = worker.worker(
self.ui,
0.1,
transfer,
(),
sorted(objects, key=lambda o: o.get(b'oid')),
)
Matt Harbison
lfs: default to not using workers for upload/download...
r35750 else:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 oids = transfer(sorted(objects, key=lambda o: o.get(b'oid')))
Matt Harbison
lfs: default to not using workers for upload/download...
r35750
Matt Harbison
lfs: add "bytes" as the unit to the upload/download progress bar...
r44534 with self.ui.makeprogress(
topic, unit=_(b"bytes"), total=total
) as progress:
Matt Harbison
lfs: use a context manager to control the progress bar lifetime
r39426 progress.update(0)
processed = 0
blobs = 0
for _one, oid in oids:
processed += sizes[oid]
blobs += 1
progress.update(processed)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 self.ui.note(_(b'lfs: processed: %s\n') % oid)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Matt Harbison
lfs: emit a status message to indicate how many blobs were uploaded...
r35899 if blobs > 0:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 if action == b'upload':
Augie Fackler
formatting: blacken the codebase...
r43346 self.ui.status(
_(b'lfs: uploaded %d files (%s)\n')
% (blobs, util.bytecount(processed))
)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 elif action == b'download':
Augie Fackler
formatting: blacken the codebase...
r43346 self.ui.status(
_(b'lfs: downloaded %d files (%s)\n')
% (blobs, util.bytecount(processed))
)
Matt Harbison
lfs: emit a status message to indicate how many blobs were uploaded...
r35899
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 def __del__(self):
# copied from mercurial/httppeer.py
urlopener = getattr(self, 'urlopener', None)
if urlopener:
for h in urlopener.handlers:
h.close()
Augie Fackler
formatting: blacken the codebase...
r43346 getattr(h, "close_all", lambda: None)()
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
class _dummyremote(object):
"""Dummy store storing blobs to temp directory."""
def __init__(self, repo, url):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 fullpath = repo.vfs.join(b'lfs', url.path)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 self.vfs = lfsvfs(fullpath)
def writebatch(self, pointers, fromstore):
Matt Harbison
lfs: deduplicate oids in the transfer...
r35945 for p in _deduplicate(pointers):
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 content = fromstore.read(p.oid(), verify=True)
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 with self.vfs(p.oid(), b'wb', atomictemp=True) as fp:
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 fp.write(content)
def readbatch(self, pointers, tostore):
Matt Harbison
lfs: deduplicate oids in the transfer...
r35945 for p in _deduplicate(pointers):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 with self.vfs(p.oid(), b'rb') as fp:
Matt Harbison
lfs: check content length after downloading content...
r44544 tostore.download(p.oid(), fp, None)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 class _nullremote(object):
"""Null store storing blobs to /dev/null."""
def __init__(self, repo, url):
pass
def writebatch(self, pointers, fromstore):
pass
def readbatch(self, pointers, tostore):
pass
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 class _promptremote(object):
"""Prompt user to set lfs.url when accessed."""
def __init__(self, repo, url):
pass
def writebatch(self, pointers, fromstore, ui=None):
self._prompt()
def readbatch(self, pointers, tostore, ui=None):
self._prompt()
def _prompt(self):
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise error.Abort(_(b'lfs.url needs to be configured'))
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 _storemap = {
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 b'https': _gitlfsremote,
b'http': _gitlfsremote,
b'file': _dummyremote,
b'null': _nullremote,
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 None: _promptremote,
}
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: deduplicate oids in the transfer...
r35945 def _deduplicate(pointers):
"""Remove any duplicate oids that exist in the list"""
reduced = util.sortdict()
for p in pointers:
reduced[p.oid()] = p
return reduced.values()
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 def _verify(oid, content):
Joerg Sonnenberger
node: import symbols explicitly...
r46729 realoid = hex(hashlib.sha256(content).digest())
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492 if realoid != oid:
Augie Fackler
formatting: blacken the codebase...
r43346 raise LfsCorruptionError(
_(b'detected corrupt lfs object: %s') % oid,
hint=_(b'run hg verify'),
)
Matt Harbison
lfs: verify lfs object content when transferring to and from the remote store...
r35492
Matt Harbison
lfs: infer the blob store URL from an explicit push dest or default-push...
r37582 def remote(repo, remote=None):
Matt Harbison
lfs: infer the blob store URL from paths.default...
r37536 """remotestore factory. return a store in _storemap depending on config
If ``lfs.url`` is specified, use that remote endpoint. Otherwise, try to
infer the endpoint, based on the remote repository using the same path
adjustments as git. As an extension, 'http' is supported as well so that
``hg serve`` works out of the box.
https://github.com/git-lfs/git-lfs/blob/master/docs/api/server-discovery.md
"""
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 lfsurl = repo.ui.config(b'lfs', b'url')
urlutil: extract `url` related code from `util` into the new module...
r47669 url = urlutil.url(lfsurl or b'')
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583 if lfsurl is None:
Matt Harbison
lfs: infer the blob store URL from an explicit push dest or default-push...
r37582 if remote:
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583 path = remote
Augie Fackler
formatting: byteify all mercurial/ and hgext/ string literals...
r43347 elif util.safehasattr(repo, b'_subtoppath'):
Matt Harbison
lfs: infer the blob store URL from an explicit pull source...
r37581 # The pull command sets this during the optional update phase, which
# tells exactly where the pull originated, whether 'paths.default'
# or explicit.
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583 path = repo._subtoppath
Matt Harbison
lfs: infer the blob store URL from an explicit pull source...
r37581 else:
# TODO: investigate 'paths.remote:lfsurl' style path customization,
# and fall back to inferring from 'paths.remote' if unspecified.
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 path = repo.ui.config(b'paths', b'default') or b''
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583
urlutil: extract `url` related code from `util` into the new module...
r47669 defaulturl = urlutil.url(path)
Matt Harbison
lfs: infer the blob store URL from paths.default...
r37536
# TODO: support local paths as well.
# TODO: consider the ssh -> https transformation that git applies
if defaulturl.scheme in (b'http', b'https'):
Matt Harbison
lfs: handle paths that don't end with '/' when inferring the blob store...
r37583 if defaulturl.path and defaulturl.path[:-1] != b'/':
defaulturl.path += b'/'
Matt Harbison
lfs: fix the inferred remote store path when using a --prefix...
r37709 defaulturl.path = (defaulturl.path or b'') + b'.git/info/lfs'
Matt Harbison
lfs: infer the blob store URL from paths.default...
r37536
urlutil: extract `url` related code from `util` into the new module...
r47669 url = urlutil.url(bytes(defaulturl))
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 repo.ui.note(_(b'lfs: assuming remote store: %s\n') % url)
Matt Harbison
lfs: infer the blob store URL from paths.default...
r37536
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 scheme = url.scheme
if scheme not in _storemap:
Matt Harbison
py3: byteify the LFS blobstore module...
r41471 raise error.Abort(_(b'lfs: unknown url scheme: %s') % scheme)
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 return _storemap[scheme](repo, url)
Augie Fackler
formatting: blacken the codebase...
r43346
Gregory Szorc
global: replace most uses of RevlogError with StorageError (API)...
r39813 class LfsRemoteError(error.StorageError):
Matt Harbison
lfs: import the Facebook git-lfs client extension...
r35097 pass
Matt Harbison
lfs: gracefully handle aborts on the server when corrupt blobs are detected...
r37710
Augie Fackler
formatting: blacken the codebase...
r43346
Matt Harbison
lfs: gracefully handle aborts on the server when corrupt blobs are detected...
r37710 class LfsCorruptionError(error.Abort):
"""Raised when a corrupt blob is detected, aborting an operation
It exists to allow specialized handling on the server side."""