##// END OF EJS Templates
lfs: add an experimental config to override User-Agent for the blob transfer...
Matt Harbison -
r35456:e333d275 default
parent child Browse files
Show More
@@ -1,201 +1,205 b''
1 # lfs - hash-preserving large file support using Git-LFS protocol
1 # lfs - hash-preserving large file support using Git-LFS protocol
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """lfs - large file support (EXPERIMENTAL)
8 """lfs - large file support (EXPERIMENTAL)
9
9
10 Configs::
10 Configs::
11
11
12 [lfs]
12 [lfs]
13 # Remote endpoint. Multiple protocols are supported:
13 # Remote endpoint. Multiple protocols are supported:
14 # - http(s)://user:pass@example.com/path
14 # - http(s)://user:pass@example.com/path
15 # git-lfs endpoint
15 # git-lfs endpoint
16 # - file:///tmp/path
16 # - file:///tmp/path
17 # local filesystem, usually for testing
17 # local filesystem, usually for testing
18 # if unset, lfs will prompt setting this when it must use this value.
18 # if unset, lfs will prompt setting this when it must use this value.
19 # (default: unset)
19 # (default: unset)
20 url = https://example.com/lfs
20 url = https://example.com/lfs
21
21
22 # size of a file to make it use LFS
22 # size of a file to make it use LFS
23 threshold = 10M
23 threshold = 10M
24
24
25 # how many times to retry before giving up on transferring an object
25 # how many times to retry before giving up on transferring an object
26 retry = 5
26 retry = 5
27
27
28 # the local directory to store lfs files for sharing across local clones.
28 # the local directory to store lfs files for sharing across local clones.
29 # If not set, the cache is located in an OS specific cache location.
29 # If not set, the cache is located in an OS specific cache location.
30 usercache = /path/to/global/cache
30 usercache = /path/to/global/cache
31 """
31 """
32
32
33 from __future__ import absolute_import
33 from __future__ import absolute_import
34
34
35 from mercurial.i18n import _
35 from mercurial.i18n import _
36
36
37 from mercurial import (
37 from mercurial import (
38 bundle2,
38 bundle2,
39 changegroup,
39 changegroup,
40 context,
40 context,
41 exchange,
41 exchange,
42 extensions,
42 extensions,
43 filelog,
43 filelog,
44 hg,
44 hg,
45 localrepo,
45 localrepo,
46 registrar,
46 registrar,
47 revlog,
47 revlog,
48 scmutil,
48 scmutil,
49 upgrade,
49 upgrade,
50 vfs as vfsmod,
50 vfs as vfsmod,
51 )
51 )
52
52
53 from . import (
53 from . import (
54 blobstore,
54 blobstore,
55 wrapper,
55 wrapper,
56 )
56 )
57
57
58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
60 # be specifying the version(s) of Mercurial they are tested with, or
60 # be specifying the version(s) of Mercurial they are tested with, or
61 # leave the attribute unspecified.
61 # leave the attribute unspecified.
62 testedwith = 'ships-with-hg-core'
62 testedwith = 'ships-with-hg-core'
63
63
64 configtable = {}
64 configtable = {}
65 configitem = registrar.configitem(configtable)
65 configitem = registrar.configitem(configtable)
66
66
67 configitem('experimental', 'lfs.user-agent',
68 default=None,
69 )
70
67 configitem('lfs', 'url',
71 configitem('lfs', 'url',
68 default=configitem.dynamicdefault,
72 default=configitem.dynamicdefault,
69 )
73 )
70 configitem('lfs', 'usercache',
74 configitem('lfs', 'usercache',
71 default=None,
75 default=None,
72 )
76 )
73 configitem('lfs', 'threshold',
77 configitem('lfs', 'threshold',
74 default=None,
78 default=None,
75 )
79 )
76 configitem('lfs', 'retry',
80 configitem('lfs', 'retry',
77 default=5,
81 default=5,
78 )
82 )
79 # Deprecated
83 # Deprecated
80 configitem('lfs', 'remotestore',
84 configitem('lfs', 'remotestore',
81 default=None,
85 default=None,
82 )
86 )
83 # Deprecated
87 # Deprecated
84 configitem('lfs', 'dummy',
88 configitem('lfs', 'dummy',
85 default=None,
89 default=None,
86 )
90 )
87 # Deprecated
91 # Deprecated
88 configitem('lfs', 'git-lfs',
92 configitem('lfs', 'git-lfs',
89 default=None,
93 default=None,
90 )
94 )
91
95
92 cmdtable = {}
96 cmdtable = {}
93 command = registrar.command(cmdtable)
97 command = registrar.command(cmdtable)
94
98
95 templatekeyword = registrar.templatekeyword()
99 templatekeyword = registrar.templatekeyword()
96
100
97 def featuresetup(ui, supported):
101 def featuresetup(ui, supported):
98 # don't die on seeing a repo with the lfs requirement
102 # don't die on seeing a repo with the lfs requirement
99 supported |= {'lfs'}
103 supported |= {'lfs'}
100
104
101 def uisetup(ui):
105 def uisetup(ui):
102 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
106 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
103
107
104 def reposetup(ui, repo):
108 def reposetup(ui, repo):
105 # Nothing to do with a remote repo
109 # Nothing to do with a remote repo
106 if not repo.local():
110 if not repo.local():
107 return
111 return
108
112
109 threshold = repo.ui.configbytes('lfs', 'threshold')
113 threshold = repo.ui.configbytes('lfs', 'threshold')
110
114
111 repo.svfs.options['lfsthreshold'] = threshold
115 repo.svfs.options['lfsthreshold'] = threshold
112 repo.svfs.lfslocalblobstore = blobstore.local(repo)
116 repo.svfs.lfslocalblobstore = blobstore.local(repo)
113 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
117 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
114
118
115 # Push hook
119 # Push hook
116 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
120 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
117
121
118 if 'lfs' not in repo.requirements:
122 if 'lfs' not in repo.requirements:
119 def checkrequireslfs(ui, repo, **kwargs):
123 def checkrequireslfs(ui, repo, **kwargs):
120 if 'lfs' not in repo.requirements:
124 if 'lfs' not in repo.requirements:
121 ctx = repo[kwargs['node']]
125 ctx = repo[kwargs['node']]
122 # TODO: is there a way to just walk the files in the commit?
126 # TODO: is there a way to just walk the files in the commit?
123 if any(ctx[f].islfs() for f in ctx.files()):
127 if any(ctx[f].islfs() for f in ctx.files()):
124 repo.requirements.add('lfs')
128 repo.requirements.add('lfs')
125 repo._writerequirements()
129 repo._writerequirements()
126
130
127 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
131 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
128
132
129 def wrapfilelog(filelog):
133 def wrapfilelog(filelog):
130 wrapfunction = extensions.wrapfunction
134 wrapfunction = extensions.wrapfunction
131
135
132 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
136 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
133 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
137 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
134 wrapfunction(filelog, 'size', wrapper.filelogsize)
138 wrapfunction(filelog, 'size', wrapper.filelogsize)
135
139
136 def extsetup(ui):
140 def extsetup(ui):
137 wrapfilelog(filelog.filelog)
141 wrapfilelog(filelog.filelog)
138
142
139 wrapfunction = extensions.wrapfunction
143 wrapfunction = extensions.wrapfunction
140
144
141 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
145 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
142
146
143 wrapfunction(upgrade, '_finishdatamigration',
147 wrapfunction(upgrade, '_finishdatamigration',
144 wrapper.upgradefinishdatamigration)
148 wrapper.upgradefinishdatamigration)
145
149
146 wrapfunction(upgrade, 'preservedrequirements',
150 wrapfunction(upgrade, 'preservedrequirements',
147 wrapper.upgraderequirements)
151 wrapper.upgraderequirements)
148
152
149 wrapfunction(upgrade, 'supporteddestrequirements',
153 wrapfunction(upgrade, 'supporteddestrequirements',
150 wrapper.upgraderequirements)
154 wrapper.upgraderequirements)
151
155
152 wrapfunction(changegroup,
156 wrapfunction(changegroup,
153 'supportedoutgoingversions',
157 'supportedoutgoingversions',
154 wrapper.supportedoutgoingversions)
158 wrapper.supportedoutgoingversions)
155 wrapfunction(changegroup,
159 wrapfunction(changegroup,
156 'allsupportedversions',
160 'allsupportedversions',
157 wrapper.allsupportedversions)
161 wrapper.allsupportedversions)
158
162
159 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
163 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
160 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
164 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
161 context.basefilectx.islfs = wrapper.filectxislfs
165 context.basefilectx.islfs = wrapper.filectxislfs
162
166
163 revlog.addflagprocessor(
167 revlog.addflagprocessor(
164 revlog.REVIDX_EXTSTORED,
168 revlog.REVIDX_EXTSTORED,
165 (
169 (
166 wrapper.readfromstore,
170 wrapper.readfromstore,
167 wrapper.writetostore,
171 wrapper.writetostore,
168 wrapper.bypasscheckhash,
172 wrapper.bypasscheckhash,
169 ),
173 ),
170 )
174 )
171
175
172 wrapfunction(hg, 'clone', wrapper.hgclone)
176 wrapfunction(hg, 'clone', wrapper.hgclone)
173 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
177 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
174
178
175 # Make bundle choose changegroup3 instead of changegroup2. This affects
179 # Make bundle choose changegroup3 instead of changegroup2. This affects
176 # "hg bundle" command. Note: it does not cover all bundle formats like
180 # "hg bundle" command. Note: it does not cover all bundle formats like
177 # "packed1". Using "packed1" with lfs will likely cause trouble.
181 # "packed1". Using "packed1" with lfs will likely cause trouble.
178 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
182 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
179 for k in names:
183 for k in names:
180 exchange._bundlespeccgversions[k] = '03'
184 exchange._bundlespeccgversions[k] = '03'
181
185
182 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
186 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
183 # options and blob stores are passed from othervfs to the new readonlyvfs.
187 # options and blob stores are passed from othervfs to the new readonlyvfs.
184 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
188 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
185
189
186 # when writing a bundle via "hg bundle" command, upload related LFS blobs
190 # when writing a bundle via "hg bundle" command, upload related LFS blobs
187 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
191 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
188
192
189 @templatekeyword('lfs_files')
193 @templatekeyword('lfs_files')
190 def lfsfiles(repo, ctx, **args):
194 def lfsfiles(repo, ctx, **args):
191 """List of strings. LFS files added or modified by the changeset."""
195 """List of strings. LFS files added or modified by the changeset."""
192 pointers = wrapper.pointersfromctx(ctx) # {path: pointer}
196 pointers = wrapper.pointersfromctx(ctx) # {path: pointer}
193 return sorted(pointers.keys())
197 return sorted(pointers.keys())
194
198
195 @command('debuglfsupload',
199 @command('debuglfsupload',
196 [('r', 'rev', [], _('upload large files introduced by REV'))])
200 [('r', 'rev', [], _('upload large files introduced by REV'))])
197 def debuglfsupload(ui, repo, **opts):
201 def debuglfsupload(ui, repo, **opts):
198 """upload lfs blobs added by the working copy parent or given revisions"""
202 """upload lfs blobs added by the working copy parent or given revisions"""
199 revs = opts.get('rev', [])
203 revs = opts.get('rev', [])
200 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
204 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
201 wrapper.uploadblobs(repo, pointers)
205 wrapper.uploadblobs(repo, pointers)
@@ -1,387 +1,389 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 json
10 import json
11 import os
11 import os
12 import re
12 import re
13
13
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15
15
16 from mercurial import (
16 from mercurial import (
17 error,
17 error,
18 pathutil,
18 pathutil,
19 url as urlmod,
19 url as urlmod,
20 util,
20 util,
21 vfs as vfsmod,
21 vfs as vfsmod,
22 worker,
22 worker,
23 )
23 )
24
24
25 from ..largefiles import lfutil
25 from ..largefiles import lfutil
26
26
27 # 64 bytes for SHA256
27 # 64 bytes for SHA256
28 _lfsre = re.compile(r'\A[a-f0-9]{64}\Z')
28 _lfsre = re.compile(r'\A[a-f0-9]{64}\Z')
29
29
30 class lfsvfs(vfsmod.vfs):
30 class lfsvfs(vfsmod.vfs):
31 def join(self, path):
31 def join(self, path):
32 """split the path at first two characters, like: XX/XXXXX..."""
32 """split the path at first two characters, like: XX/XXXXX..."""
33 if not _lfsre.match(path):
33 if not _lfsre.match(path):
34 raise error.ProgrammingError('unexpected lfs path: %s' % path)
34 raise error.ProgrammingError('unexpected lfs path: %s' % path)
35 return super(lfsvfs, self).join(path[0:2], path[2:])
35 return super(lfsvfs, self).join(path[0:2], path[2:])
36
36
37 def walk(self, path=None, onerror=None):
37 def walk(self, path=None, onerror=None):
38 """Yield (dirpath, [], oids) tuple for blobs under path
38 """Yield (dirpath, [], oids) tuple for blobs under path
39
39
40 Oids only exist in the root of this vfs, so dirpath is always ''.
40 Oids only exist in the root of this vfs, so dirpath is always ''.
41 """
41 """
42 root = os.path.normpath(self.base)
42 root = os.path.normpath(self.base)
43 # when dirpath == root, dirpath[prefixlen:] becomes empty
43 # when dirpath == root, dirpath[prefixlen:] becomes empty
44 # because len(dirpath) < prefixlen.
44 # because len(dirpath) < prefixlen.
45 prefixlen = len(pathutil.normasprefix(root))
45 prefixlen = len(pathutil.normasprefix(root))
46 oids = []
46 oids = []
47
47
48 for dirpath, dirs, files in os.walk(self.reljoin(self.base, path or ''),
48 for dirpath, dirs, files in os.walk(self.reljoin(self.base, path or ''),
49 onerror=onerror):
49 onerror=onerror):
50 dirpath = dirpath[prefixlen:]
50 dirpath = dirpath[prefixlen:]
51
51
52 # Silently skip unexpected files and directories
52 # Silently skip unexpected files and directories
53 if len(dirpath) == 2:
53 if len(dirpath) == 2:
54 oids.extend([dirpath + f for f in files
54 oids.extend([dirpath + f for f in files
55 if _lfsre.match(dirpath + f)])
55 if _lfsre.match(dirpath + f)])
56
56
57 yield ('', [], oids)
57 yield ('', [], oids)
58
58
59 class filewithprogress(object):
59 class filewithprogress(object):
60 """a file-like object that supports __len__ and read.
60 """a file-like object that supports __len__ and read.
61
61
62 Useful to provide progress information for how many bytes are read.
62 Useful to provide progress information for how many bytes are read.
63 """
63 """
64
64
65 def __init__(self, fp, callback):
65 def __init__(self, fp, callback):
66 self._fp = fp
66 self._fp = fp
67 self._callback = callback # func(readsize)
67 self._callback = callback # func(readsize)
68 fp.seek(0, os.SEEK_END)
68 fp.seek(0, os.SEEK_END)
69 self._len = fp.tell()
69 self._len = fp.tell()
70 fp.seek(0)
70 fp.seek(0)
71
71
72 def __len__(self):
72 def __len__(self):
73 return self._len
73 return self._len
74
74
75 def read(self, size):
75 def read(self, size):
76 if self._fp is None:
76 if self._fp is None:
77 return b''
77 return b''
78 data = self._fp.read(size)
78 data = self._fp.read(size)
79 if data:
79 if data:
80 if self._callback:
80 if self._callback:
81 self._callback(len(data))
81 self._callback(len(data))
82 else:
82 else:
83 self._fp.close()
83 self._fp.close()
84 self._fp = None
84 self._fp = None
85 return data
85 return data
86
86
87 class local(object):
87 class local(object):
88 """Local blobstore for large file contents.
88 """Local blobstore for large file contents.
89
89
90 This blobstore is used both as a cache and as a staging area for large blobs
90 This blobstore is used both as a cache and as a staging area for large blobs
91 to be uploaded to the remote blobstore.
91 to be uploaded to the remote blobstore.
92 """
92 """
93
93
94 def __init__(self, repo):
94 def __init__(self, repo):
95 fullpath = repo.svfs.join('lfs/objects')
95 fullpath = repo.svfs.join('lfs/objects')
96 self.vfs = lfsvfs(fullpath)
96 self.vfs = lfsvfs(fullpath)
97 usercache = lfutil._usercachedir(repo.ui, 'lfs')
97 usercache = lfutil._usercachedir(repo.ui, 'lfs')
98 self.cachevfs = lfsvfs(usercache)
98 self.cachevfs = lfsvfs(usercache)
99
99
100 def write(self, oid, data):
100 def write(self, oid, data):
101 """Write blob to local blobstore."""
101 """Write blob to local blobstore."""
102 with self.vfs(oid, 'wb', atomictemp=True) as fp:
102 with self.vfs(oid, 'wb', atomictemp=True) as fp:
103 fp.write(data)
103 fp.write(data)
104
104
105 # XXX: should we verify the content of the cache, and hardlink back to
105 # XXX: should we verify the content of the cache, and hardlink back to
106 # the local store on success, but truncate, write and link on failure?
106 # the local store on success, but truncate, write and link on failure?
107 if not self.cachevfs.exists(oid):
107 if not self.cachevfs.exists(oid):
108 lfutil.link(self.vfs.join(oid), self.cachevfs.join(oid))
108 lfutil.link(self.vfs.join(oid), self.cachevfs.join(oid))
109
109
110 def read(self, oid):
110 def read(self, oid):
111 """Read blob from local blobstore."""
111 """Read blob from local blobstore."""
112 if not self.vfs.exists(oid):
112 if not self.vfs.exists(oid):
113 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
113 lfutil.link(self.cachevfs.join(oid), self.vfs.join(oid))
114 return self.vfs.read(oid)
114 return self.vfs.read(oid)
115
115
116 def has(self, oid):
116 def has(self, oid):
117 """Returns True if the local blobstore contains the requested blob,
117 """Returns True if the local blobstore contains the requested blob,
118 False otherwise."""
118 False otherwise."""
119 return self.cachevfs.exists(oid) or self.vfs.exists(oid)
119 return self.cachevfs.exists(oid) or self.vfs.exists(oid)
120
120
121 class _gitlfsremote(object):
121 class _gitlfsremote(object):
122
122
123 def __init__(self, repo, url):
123 def __init__(self, repo, url):
124 ui = repo.ui
124 ui = repo.ui
125 self.ui = ui
125 self.ui = ui
126 baseurl, authinfo = url.authinfo()
126 baseurl, authinfo = url.authinfo()
127 self.baseurl = baseurl.rstrip('/')
127 self.baseurl = baseurl.rstrip('/')
128 useragent = 'mercurial/%s git/2.15.1' % util.version()
128 useragent = repo.ui.config('experimental', 'lfs.user-agent')
129 if not useragent:
130 useragent = 'mercurial/%s git/2.15.1' % util.version()
129 self.urlopener = urlmod.opener(ui, authinfo, useragent)
131 self.urlopener = urlmod.opener(ui, authinfo, useragent)
130 self.retry = ui.configint('lfs', 'retry')
132 self.retry = ui.configint('lfs', 'retry')
131
133
132 def writebatch(self, pointers, fromstore):
134 def writebatch(self, pointers, fromstore):
133 """Batch upload from local to remote blobstore."""
135 """Batch upload from local to remote blobstore."""
134 self._batch(pointers, fromstore, 'upload')
136 self._batch(pointers, fromstore, 'upload')
135
137
136 def readbatch(self, pointers, tostore):
138 def readbatch(self, pointers, tostore):
137 """Batch download from remote to local blostore."""
139 """Batch download from remote to local blostore."""
138 self._batch(pointers, tostore, 'download')
140 self._batch(pointers, tostore, 'download')
139
141
140 def _batchrequest(self, pointers, action):
142 def _batchrequest(self, pointers, action):
141 """Get metadata about objects pointed by pointers for given action
143 """Get metadata about objects pointed by pointers for given action
142
144
143 Return decoded JSON object like {'objects': [{'oid': '', 'size': 1}]}
145 Return decoded JSON object like {'objects': [{'oid': '', 'size': 1}]}
144 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
146 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
145 """
147 """
146 objects = [{'oid': p.oid(), 'size': p.size()} for p in pointers]
148 objects = [{'oid': p.oid(), 'size': p.size()} for p in pointers]
147 requestdata = json.dumps({
149 requestdata = json.dumps({
148 'objects': objects,
150 'objects': objects,
149 'operation': action,
151 'operation': action,
150 })
152 })
151 batchreq = util.urlreq.request('%s/objects/batch' % self.baseurl,
153 batchreq = util.urlreq.request('%s/objects/batch' % self.baseurl,
152 data=requestdata)
154 data=requestdata)
153 batchreq.add_header('Accept', 'application/vnd.git-lfs+json')
155 batchreq.add_header('Accept', 'application/vnd.git-lfs+json')
154 batchreq.add_header('Content-Type', 'application/vnd.git-lfs+json')
156 batchreq.add_header('Content-Type', 'application/vnd.git-lfs+json')
155 try:
157 try:
156 rawjson = self.urlopener.open(batchreq).read()
158 rawjson = self.urlopener.open(batchreq).read()
157 except util.urlerr.httperror as ex:
159 except util.urlerr.httperror as ex:
158 raise LfsRemoteError(_('LFS HTTP error: %s (action=%s)')
160 raise LfsRemoteError(_('LFS HTTP error: %s (action=%s)')
159 % (ex, action))
161 % (ex, action))
160 try:
162 try:
161 response = json.loads(rawjson)
163 response = json.loads(rawjson)
162 except ValueError:
164 except ValueError:
163 raise LfsRemoteError(_('LFS server returns invalid JSON: %s')
165 raise LfsRemoteError(_('LFS server returns invalid JSON: %s')
164 % rawjson)
166 % rawjson)
165 return response
167 return response
166
168
167 def _checkforservererror(self, pointers, responses):
169 def _checkforservererror(self, pointers, responses):
168 """Scans errors from objects
170 """Scans errors from objects
169
171
170 Returns LfsRemoteError if any objects has an error"""
172 Returns LfsRemoteError if any objects has an error"""
171 for response in responses:
173 for response in responses:
172 error = response.get('error')
174 error = response.get('error')
173 if error:
175 if error:
174 ptrmap = {p.oid(): p for p in pointers}
176 ptrmap = {p.oid(): p for p in pointers}
175 p = ptrmap.get(response['oid'], None)
177 p = ptrmap.get(response['oid'], None)
176 if error['code'] == 404 and p:
178 if error['code'] == 404 and p:
177 filename = getattr(p, 'filename', 'unknown')
179 filename = getattr(p, 'filename', 'unknown')
178 raise LfsRemoteError(
180 raise LfsRemoteError(
179 _(('LFS server error. Remote object '
181 _(('LFS server error. Remote object '
180 'for file %s not found: %r')) % (filename, response))
182 'for file %s not found: %r')) % (filename, response))
181 raise LfsRemoteError(_('LFS server error: %r') % response)
183 raise LfsRemoteError(_('LFS server error: %r') % response)
182
184
183 def _extractobjects(self, response, pointers, action):
185 def _extractobjects(self, response, pointers, action):
184 """extract objects from response of the batch API
186 """extract objects from response of the batch API
185
187
186 response: parsed JSON object returned by batch API
188 response: parsed JSON object returned by batch API
187 return response['objects'] filtered by action
189 return response['objects'] filtered by action
188 raise if any object has an error
190 raise if any object has an error
189 """
191 """
190 # Scan errors from objects - fail early
192 # Scan errors from objects - fail early
191 objects = response.get('objects', [])
193 objects = response.get('objects', [])
192 self._checkforservererror(pointers, objects)
194 self._checkforservererror(pointers, objects)
193
195
194 # Filter objects with given action. Practically, this skips uploading
196 # Filter objects with given action. Practically, this skips uploading
195 # objects which exist in the server.
197 # objects which exist in the server.
196 filteredobjects = [o for o in objects if action in o.get('actions', [])]
198 filteredobjects = [o for o in objects if action in o.get('actions', [])]
197 # But for downloading, we want all objects. Therefore missing objects
199 # But for downloading, we want all objects. Therefore missing objects
198 # should be considered an error.
200 # should be considered an error.
199 if action == 'download':
201 if action == 'download':
200 if len(filteredobjects) < len(objects):
202 if len(filteredobjects) < len(objects):
201 missing = [o.get('oid', '?')
203 missing = [o.get('oid', '?')
202 for o in objects
204 for o in objects
203 if action not in o.get('actions', [])]
205 if action not in o.get('actions', [])]
204 raise LfsRemoteError(
206 raise LfsRemoteError(
205 _('LFS server claims required objects do not exist:\n%s')
207 _('LFS server claims required objects do not exist:\n%s')
206 % '\n'.join(missing))
208 % '\n'.join(missing))
207
209
208 return filteredobjects
210 return filteredobjects
209
211
210 def _basictransfer(self, obj, action, localstore):
212 def _basictransfer(self, obj, action, localstore):
211 """Download or upload a single object using basic transfer protocol
213 """Download or upload a single object using basic transfer protocol
212
214
213 obj: dict, an object description returned by batch API
215 obj: dict, an object description returned by batch API
214 action: string, one of ['upload', 'download']
216 action: string, one of ['upload', 'download']
215 localstore: blobstore.local
217 localstore: blobstore.local
216
218
217 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/\
219 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/\
218 basic-transfers.md
220 basic-transfers.md
219 """
221 """
220 oid = str(obj['oid'])
222 oid = str(obj['oid'])
221
223
222 href = str(obj['actions'][action].get('href'))
224 href = str(obj['actions'][action].get('href'))
223 headers = obj['actions'][action].get('header', {}).items()
225 headers = obj['actions'][action].get('header', {}).items()
224
226
225 request = util.urlreq.request(href)
227 request = util.urlreq.request(href)
226 if action == 'upload':
228 if action == 'upload':
227 # If uploading blobs, read data from local blobstore.
229 # If uploading blobs, read data from local blobstore.
228 request.data = filewithprogress(localstore.vfs(oid), None)
230 request.data = filewithprogress(localstore.vfs(oid), None)
229 request.get_method = lambda: 'PUT'
231 request.get_method = lambda: 'PUT'
230
232
231 for k, v in headers:
233 for k, v in headers:
232 request.add_header(k, v)
234 request.add_header(k, v)
233
235
234 response = b''
236 response = b''
235 try:
237 try:
236 req = self.urlopener.open(request)
238 req = self.urlopener.open(request)
237 while True:
239 while True:
238 data = req.read(1048576)
240 data = req.read(1048576)
239 if not data:
241 if not data:
240 break
242 break
241 response += data
243 response += data
242 except util.urlerr.httperror as ex:
244 except util.urlerr.httperror as ex:
243 raise LfsRemoteError(_('HTTP error: %s (oid=%s, action=%s)')
245 raise LfsRemoteError(_('HTTP error: %s (oid=%s, action=%s)')
244 % (ex, oid, action))
246 % (ex, oid, action))
245
247
246 if action == 'download':
248 if action == 'download':
247 # If downloading blobs, store downloaded data to local blobstore
249 # If downloading blobs, store downloaded data to local blobstore
248 localstore.write(oid, response)
250 localstore.write(oid, response)
249
251
250 def _batch(self, pointers, localstore, action):
252 def _batch(self, pointers, localstore, action):
251 if action not in ['upload', 'download']:
253 if action not in ['upload', 'download']:
252 raise error.ProgrammingError('invalid Git-LFS action: %s' % action)
254 raise error.ProgrammingError('invalid Git-LFS action: %s' % action)
253
255
254 response = self._batchrequest(pointers, action)
256 response = self._batchrequest(pointers, action)
255 objects = self._extractobjects(response, pointers, action)
257 objects = self._extractobjects(response, pointers, action)
256 total = sum(x.get('size', 0) for x in objects)
258 total = sum(x.get('size', 0) for x in objects)
257 sizes = {}
259 sizes = {}
258 for obj in objects:
260 for obj in objects:
259 sizes[obj.get('oid')] = obj.get('size', 0)
261 sizes[obj.get('oid')] = obj.get('size', 0)
260 topic = {'upload': _('lfs uploading'),
262 topic = {'upload': _('lfs uploading'),
261 'download': _('lfs downloading')}[action]
263 'download': _('lfs downloading')}[action]
262 if self.ui.verbose and len(objects) > 1:
264 if self.ui.verbose and len(objects) > 1:
263 self.ui.write(_('lfs: need to transfer %d objects (%s)\n')
265 self.ui.write(_('lfs: need to transfer %d objects (%s)\n')
264 % (len(objects), util.bytecount(total)))
266 % (len(objects), util.bytecount(total)))
265 self.ui.progress(topic, 0, total=total)
267 self.ui.progress(topic, 0, total=total)
266 def transfer(chunk):
268 def transfer(chunk):
267 for obj in chunk:
269 for obj in chunk:
268 objsize = obj.get('size', 0)
270 objsize = obj.get('size', 0)
269 if self.ui.verbose:
271 if self.ui.verbose:
270 if action == 'download':
272 if action == 'download':
271 msg = _('lfs: downloading %s (%s)\n')
273 msg = _('lfs: downloading %s (%s)\n')
272 elif action == 'upload':
274 elif action == 'upload':
273 msg = _('lfs: uploading %s (%s)\n')
275 msg = _('lfs: uploading %s (%s)\n')
274 self.ui.write(msg % (obj.get('oid'),
276 self.ui.write(msg % (obj.get('oid'),
275 util.bytecount(objsize)))
277 util.bytecount(objsize)))
276 retry = self.retry
278 retry = self.retry
277 while True:
279 while True:
278 try:
280 try:
279 self._basictransfer(obj, action, localstore)
281 self._basictransfer(obj, action, localstore)
280 yield 1, obj.get('oid')
282 yield 1, obj.get('oid')
281 break
283 break
282 except Exception as ex:
284 except Exception as ex:
283 if retry > 0:
285 if retry > 0:
284 if self.ui.verbose:
286 if self.ui.verbose:
285 self.ui.write(
287 self.ui.write(
286 _('lfs: failed: %r (remaining retry %d)\n')
288 _('lfs: failed: %r (remaining retry %d)\n')
287 % (ex, retry))
289 % (ex, retry))
288 retry -= 1
290 retry -= 1
289 continue
291 continue
290 raise
292 raise
291
293
292 oids = worker.worker(self.ui, 0.1, transfer, (),
294 oids = worker.worker(self.ui, 0.1, transfer, (),
293 sorted(objects, key=lambda o: o.get('oid')))
295 sorted(objects, key=lambda o: o.get('oid')))
294 processed = 0
296 processed = 0
295 for _one, oid in oids:
297 for _one, oid in oids:
296 processed += sizes[oid]
298 processed += sizes[oid]
297 self.ui.progress(topic, processed, total=total)
299 self.ui.progress(topic, processed, total=total)
298 if self.ui.verbose:
300 if self.ui.verbose:
299 self.ui.write(_('lfs: processed: %s\n') % oid)
301 self.ui.write(_('lfs: processed: %s\n') % oid)
300 self.ui.progress(topic, pos=None, total=total)
302 self.ui.progress(topic, pos=None, total=total)
301
303
302 def __del__(self):
304 def __del__(self):
303 # copied from mercurial/httppeer.py
305 # copied from mercurial/httppeer.py
304 urlopener = getattr(self, 'urlopener', None)
306 urlopener = getattr(self, 'urlopener', None)
305 if urlopener:
307 if urlopener:
306 for h in urlopener.handlers:
308 for h in urlopener.handlers:
307 h.close()
309 h.close()
308 getattr(h, "close_all", lambda : None)()
310 getattr(h, "close_all", lambda : None)()
309
311
310 class _dummyremote(object):
312 class _dummyremote(object):
311 """Dummy store storing blobs to temp directory."""
313 """Dummy store storing blobs to temp directory."""
312
314
313 def __init__(self, repo, url):
315 def __init__(self, repo, url):
314 fullpath = repo.vfs.join('lfs', url.path)
316 fullpath = repo.vfs.join('lfs', url.path)
315 self.vfs = lfsvfs(fullpath)
317 self.vfs = lfsvfs(fullpath)
316
318
317 def writebatch(self, pointers, fromstore):
319 def writebatch(self, pointers, fromstore):
318 for p in pointers:
320 for p in pointers:
319 content = fromstore.read(p.oid())
321 content = fromstore.read(p.oid())
320 with self.vfs(p.oid(), 'wb', atomictemp=True) as fp:
322 with self.vfs(p.oid(), 'wb', atomictemp=True) as fp:
321 fp.write(content)
323 fp.write(content)
322
324
323 def readbatch(self, pointers, tostore):
325 def readbatch(self, pointers, tostore):
324 for p in pointers:
326 for p in pointers:
325 content = self.vfs.read(p.oid())
327 content = self.vfs.read(p.oid())
326 tostore.write(p.oid(), content)
328 tostore.write(p.oid(), content)
327
329
328 class _nullremote(object):
330 class _nullremote(object):
329 """Null store storing blobs to /dev/null."""
331 """Null store storing blobs to /dev/null."""
330
332
331 def __init__(self, repo, url):
333 def __init__(self, repo, url):
332 pass
334 pass
333
335
334 def writebatch(self, pointers, fromstore):
336 def writebatch(self, pointers, fromstore):
335 pass
337 pass
336
338
337 def readbatch(self, pointers, tostore):
339 def readbatch(self, pointers, tostore):
338 pass
340 pass
339
341
340 class _promptremote(object):
342 class _promptremote(object):
341 """Prompt user to set lfs.url when accessed."""
343 """Prompt user to set lfs.url when accessed."""
342
344
343 def __init__(self, repo, url):
345 def __init__(self, repo, url):
344 pass
346 pass
345
347
346 def writebatch(self, pointers, fromstore, ui=None):
348 def writebatch(self, pointers, fromstore, ui=None):
347 self._prompt()
349 self._prompt()
348
350
349 def readbatch(self, pointers, tostore, ui=None):
351 def readbatch(self, pointers, tostore, ui=None):
350 self._prompt()
352 self._prompt()
351
353
352 def _prompt(self):
354 def _prompt(self):
353 raise error.Abort(_('lfs.url needs to be configured'))
355 raise error.Abort(_('lfs.url needs to be configured'))
354
356
355 _storemap = {
357 _storemap = {
356 'https': _gitlfsremote,
358 'https': _gitlfsremote,
357 'http': _gitlfsremote,
359 'http': _gitlfsremote,
358 'file': _dummyremote,
360 'file': _dummyremote,
359 'null': _nullremote,
361 'null': _nullremote,
360 None: _promptremote,
362 None: _promptremote,
361 }
363 }
362
364
363 def remote(repo):
365 def remote(repo):
364 """remotestore factory. return a store in _storemap depending on config"""
366 """remotestore factory. return a store in _storemap depending on config"""
365 defaulturl = ''
367 defaulturl = ''
366
368
367 # convert deprecated configs to the new url. TODO: remove this if other
369 # convert deprecated configs to the new url. TODO: remove this if other
368 # places are migrated to the new url config.
370 # places are migrated to the new url config.
369 # deprecated config: lfs.remotestore
371 # deprecated config: lfs.remotestore
370 deprecatedstore = repo.ui.config('lfs', 'remotestore')
372 deprecatedstore = repo.ui.config('lfs', 'remotestore')
371 if deprecatedstore == 'dummy':
373 if deprecatedstore == 'dummy':
372 # deprecated config: lfs.remotepath
374 # deprecated config: lfs.remotepath
373 defaulturl = 'file://' + repo.ui.config('lfs', 'remotepath')
375 defaulturl = 'file://' + repo.ui.config('lfs', 'remotepath')
374 elif deprecatedstore == 'git-lfs':
376 elif deprecatedstore == 'git-lfs':
375 # deprecated config: lfs.remoteurl
377 # deprecated config: lfs.remoteurl
376 defaulturl = repo.ui.config('lfs', 'remoteurl')
378 defaulturl = repo.ui.config('lfs', 'remoteurl')
377 elif deprecatedstore == 'null':
379 elif deprecatedstore == 'null':
378 defaulturl = 'null://'
380 defaulturl = 'null://'
379
381
380 url = util.url(repo.ui.config('lfs', 'url', defaulturl))
382 url = util.url(repo.ui.config('lfs', 'url', defaulturl))
381 scheme = url.scheme
383 scheme = url.scheme
382 if scheme not in _storemap:
384 if scheme not in _storemap:
383 raise error.Abort(_('lfs: unknown url scheme: %s') % scheme)
385 raise error.Abort(_('lfs: unknown url scheme: %s') % scheme)
384 return _storemap[scheme](repo, url)
386 return _storemap[scheme](repo, url)
385
387
386 class LfsRemoteError(error.RevlogError):
388 class LfsRemoteError(error.RevlogError):
387 pass
389 pass
General Comments 0
You need to be logged in to leave comments. Login now