##// END OF EJS Templates
lfs: import the Facebook git-lfs client extension...
Matt Harbison -
r35097:66c5a8cf default
parent child Browse files
Show More
@@ -0,0 +1,132 b''
1 # lfs - hash-preserving large file support using Git-LFS protocol
2 #
3 # Copyright 2017 Facebook, Inc.
4 #
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.
7
8 """lfs - large file support (EXPERIMENTAL)
9
10 Configs::
11
12 [lfs]
13 # Remote endpoint. Multiple protocols are supported:
14 # - http(s)://user:pass@example.com/path
15 # git-lfs endpoint
16 # - file:///tmp/path
17 # local filesystem, usually for testing
18 # if unset, lfs will prompt setting this when it must use this value.
19 # (default: unset)
20 url = https://example.com/lfs
21
22 # size of a file to make it use LFS
23 threshold = 10M
24
25 # how many times to retry before giving up on transferring an object
26 retry = 5
27 """
28
29 from __future__ import absolute_import
30
31 from mercurial import (
32 bundle2,
33 changegroup,
34 context,
35 exchange,
36 extensions,
37 filelog,
38 registrar,
39 revlog,
40 scmutil,
41 vfs as vfsmod,
42 )
43 from mercurial.i18n import _
44
45 from . import (
46 blobstore,
47 wrapper,
48 )
49
50 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
51 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
52 # be specifying the version(s) of Mercurial they are tested with, or
53 # leave the attribute unspecified.
54 testedwith = 'ships-with-hg-core'
55
56 cmdtable = {}
57 command = registrar.command(cmdtable)
58
59 templatekeyword = registrar.templatekeyword()
60
61 def reposetup(ui, repo):
62 # Nothing to do with a remote repo
63 if not repo.local():
64 return
65
66 threshold = repo.ui.configbytes('lfs', 'threshold', None)
67
68 repo.svfs.options['lfsthreshold'] = threshold
69 repo.svfs.lfslocalblobstore = blobstore.local(repo)
70 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
71
72 # Push hook
73 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
74
75 def wrapfilelog(filelog):
76 wrapfunction = extensions.wrapfunction
77
78 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
79 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
80 wrapfunction(filelog, 'size', wrapper.filelogsize)
81
82 def extsetup(ui):
83 wrapfilelog(filelog.filelog)
84
85 wrapfunction = extensions.wrapfunction
86 wrapfunction(changegroup,
87 'supportedoutgoingversions',
88 wrapper.supportedoutgoingversions)
89 wrapfunction(changegroup,
90 'allsupportedversions',
91 wrapper.allsupportedversions)
92
93 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
94 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
95 context.basefilectx.islfs = wrapper.filectxislfs
96
97 revlog.addflagprocessor(
98 revlog.REVIDX_EXTSTORED,
99 (
100 wrapper.readfromstore,
101 wrapper.writetostore,
102 wrapper.bypasscheckhash,
103 ),
104 )
105
106 # Make bundle choose changegroup3 instead of changegroup2. This affects
107 # "hg bundle" command. Note: it does not cover all bundle formats like
108 # "packed1". Using "packed1" with lfs will likely cause trouble.
109 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
110 for k in names:
111 exchange._bundlespeccgversions[k] = '03'
112
113 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
114 # options and blob stores are passed from othervfs to the new readonlyvfs.
115 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
116
117 # when writing a bundle via "hg bundle" command, upload related LFS blobs
118 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
119
120 @templatekeyword('lfs_files')
121 def lfsfiles(repo, ctx, **args):
122 """List of strings. LFS files added or modified by the changeset."""
123 pointers = wrapper.pointersfromctx(ctx) # {path: pointer}
124 return sorted(pointers.keys())
125
126 @command('debuglfsupload',
127 [('r', 'rev', [], _('upload large files introduced by REV'))])
128 def debuglfsupload(ui, repo, **opts):
129 """upload lfs blobs added by the working copy parent or given revisions"""
130 revs = opts.get('rev', [])
131 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
132 wrapper.uploadblobs(repo, pointers)
@@ -0,0 +1,346 b''
1 # blobstore.py - local and remote (speaking Git-LFS protocol) blob storages
2 #
3 # Copyright 2017 Facebook, Inc.
4 #
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.
7
8 from __future__ import absolute_import
9
10 import json
11 import os
12 import re
13
14 from mercurial import (
15 error,
16 url as urlmod,
17 util,
18 vfs as vfsmod,
19 )
20 from mercurial.i18n import _
21
22 # 64 bytes for SHA256
23 _lfsre = re.compile(r'\A[a-f0-9]{64}\Z')
24
25 class lfsvfs(vfsmod.vfs):
26 def join(self, path):
27 """split the path at first two characters, like: XX/XXXXX..."""
28 if not _lfsre.match(path):
29 raise error.ProgrammingError('unexpected lfs path: %s' % path)
30 return super(lfsvfs, self).join(path[0:2], path[2:])
31
32 class filewithprogress(object):
33 """a file-like object that supports __len__ and read.
34
35 Useful to provide progress information for how many bytes are read.
36 """
37
38 def __init__(self, fp, callback):
39 self._fp = fp
40 self._callback = callback # func(readsize)
41 fp.seek(0, os.SEEK_END)
42 self._len = fp.tell()
43 fp.seek(0)
44
45 def __len__(self):
46 return self._len
47
48 def read(self, size):
49 if self._fp is None:
50 return b''
51 data = self._fp.read(size)
52 if data:
53 if self._callback:
54 self._callback(len(data))
55 else:
56 self._fp.close()
57 self._fp = None
58 return data
59
60 class local(object):
61 """Local blobstore for large file contents.
62
63 This blobstore is used both as a cache and as a staging area for large blobs
64 to be uploaded to the remote blobstore.
65 """
66
67 def __init__(self, repo):
68 fullpath = repo.svfs.join('lfs/objects')
69 self.vfs = lfsvfs(fullpath)
70
71 def write(self, oid, data):
72 """Write blob to local blobstore."""
73 with self.vfs(oid, 'wb', atomictemp=True) as fp:
74 fp.write(data)
75
76 def read(self, oid):
77 """Read blob from local blobstore."""
78 return self.vfs.read(oid)
79
80 def has(self, oid):
81 """Returns True if the local blobstore contains the requested blob,
82 False otherwise."""
83 return self.vfs.exists(oid)
84
85 class _gitlfsremote(object):
86
87 def __init__(self, repo, url):
88 ui = repo.ui
89 self.ui = ui
90 baseurl, authinfo = url.authinfo()
91 self.baseurl = baseurl.rstrip('/')
92 self.urlopener = urlmod.opener(ui, authinfo)
93 self.retry = ui.configint('lfs', 'retry', 5)
94
95 def writebatch(self, pointers, fromstore):
96 """Batch upload from local to remote blobstore."""
97 self._batch(pointers, fromstore, 'upload')
98
99 def readbatch(self, pointers, tostore):
100 """Batch download from remote to local blostore."""
101 self._batch(pointers, tostore, 'download')
102
103 def _batchrequest(self, pointers, action):
104 """Get metadata about objects pointed by pointers for given action
105
106 Return decoded JSON object like {'objects': [{'oid': '', 'size': 1}]}
107 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
108 """
109 objects = [{'oid': p.oid(), 'size': p.size()} for p in pointers]
110 requestdata = json.dumps({
111 'objects': objects,
112 'operation': action,
113 })
114 batchreq = util.urlreq.request('%s/objects/batch' % self.baseurl,
115 data=requestdata)
116 batchreq.add_header('Accept', 'application/vnd.git-lfs+json')
117 batchreq.add_header('Content-Type', 'application/vnd.git-lfs+json')
118 try:
119 rawjson = self.urlopener.open(batchreq).read()
120 except util.urlerr.httperror as ex:
121 raise LfsRemoteError(_('LFS HTTP error: %s (action=%s)')
122 % (ex, action))
123 try:
124 response = json.loads(rawjson)
125 except ValueError:
126 raise LfsRemoteError(_('LFS server returns invalid JSON: %s')
127 % rawjson)
128 return response
129
130 def _checkforservererror(self, pointers, responses):
131 """Scans errors from objects
132
133 Returns LfsRemoteError if any objects has an error"""
134 for response in responses:
135 error = response.get('error')
136 if error:
137 ptrmap = {p.oid(): p for p in pointers}
138 p = ptrmap.get(response['oid'], None)
139 if error['code'] == 404 and p:
140 filename = getattr(p, 'filename', 'unknown')
141 raise LfsRemoteError(
142 _(('LFS server error. Remote object '
143 'for file %s not found: %r')) % (filename, response))
144 raise LfsRemoteError(_('LFS server error: %r') % response)
145
146 def _extractobjects(self, response, pointers, action):
147 """extract objects from response of the batch API
148
149 response: parsed JSON object returned by batch API
150 return response['objects'] filtered by action
151 raise if any object has an error
152 """
153 # Scan errors from objects - fail early
154 objects = response.get('objects', [])
155 self._checkforservererror(pointers, objects)
156
157 # Filter objects with given action. Practically, this skips uploading
158 # objects which exist in the server.
159 filteredobjects = [o for o in objects if action in o.get('actions', [])]
160 # But for downloading, we want all objects. Therefore missing objects
161 # should be considered an error.
162 if action == 'download':
163 if len(filteredobjects) < len(objects):
164 missing = [o.get('oid', '?')
165 for o in objects
166 if action not in o.get('actions', [])]
167 raise LfsRemoteError(
168 _('LFS server claims required objects do not exist:\n%s')
169 % '\n'.join(missing))
170
171 return filteredobjects
172
173 def _basictransfer(self, obj, action, localstore, progress=None):
174 """Download or upload a single object using basic transfer protocol
175
176 obj: dict, an object description returned by batch API
177 action: string, one of ['upload', 'download']
178 localstore: blobstore.local
179
180 See https://github.com/git-lfs/git-lfs/blob/master/docs/api/\
181 basic-transfers.md
182 """
183 oid = str(obj['oid'])
184
185 href = str(obj['actions'][action].get('href'))
186 headers = obj['actions'][action].get('header', {}).items()
187
188 request = util.urlreq.request(href)
189 if action == 'upload':
190 # If uploading blobs, read data from local blobstore.
191 request.data = filewithprogress(localstore.vfs(oid), progress)
192 request.get_method = lambda: 'PUT'
193
194 for k, v in headers:
195 request.add_header(k, v)
196
197 response = b''
198 try:
199 req = self.urlopener.open(request)
200 while True:
201 data = req.read(1048576)
202 if not data:
203 break
204 if action == 'download' and progress:
205 progress(len(data))
206 response += data
207 except util.urlerr.httperror as ex:
208 raise LfsRemoteError(_('HTTP error: %s (oid=%s, action=%s)')
209 % (ex, oid, action))
210
211 if action == 'download':
212 # If downloading blobs, store downloaded data to local blobstore
213 localstore.write(oid, response)
214
215 def _batch(self, pointers, localstore, action):
216 if action not in ['upload', 'download']:
217 raise error.ProgrammingError('invalid Git-LFS action: %s' % action)
218
219 response = self._batchrequest(pointers, action)
220 prunningsize = [0]
221 objects = self._extractobjects(response, pointers, action)
222 total = sum(x.get('size', 0) for x in objects)
223 topic = {'upload': _('lfs uploading'),
224 'download': _('lfs downloading')}[action]
225 if self.ui.verbose and len(objects) > 1:
226 self.ui.write(_('lfs: need to transfer %d objects (%s)\n')
227 % (len(objects), util.bytecount(total)))
228 self.ui.progress(topic, 0, total=total)
229 def progress(size):
230 # advance progress bar by "size" bytes
231 prunningsize[0] += size
232 self.ui.progress(topic, prunningsize[0], total=total)
233 for obj in sorted(objects, key=lambda o: o.get('oid')):
234 objsize = obj.get('size', 0)
235 if self.ui.verbose:
236 if action == 'download':
237 msg = _('lfs: downloading %s (%s)\n')
238 elif action == 'upload':
239 msg = _('lfs: uploading %s (%s)\n')
240 self.ui.write(msg % (obj.get('oid'), util.bytecount(objsize)))
241 origrunningsize = prunningsize[0]
242 retry = self.retry
243 while True:
244 prunningsize[0] = origrunningsize
245 try:
246 self._basictransfer(obj, action, localstore,
247 progress=progress)
248 break
249 except Exception as ex:
250 if retry > 0:
251 if self.ui.verbose:
252 self.ui.write(
253 _('lfs: failed: %r (remaining retry %d)\n')
254 % (ex, retry))
255 retry -= 1
256 continue
257 raise
258
259 self.ui.progress(topic, pos=None, total=total)
260
261 def __del__(self):
262 # copied from mercurial/httppeer.py
263 urlopener = getattr(self, 'urlopener', None)
264 if urlopener:
265 for h in urlopener.handlers:
266 h.close()
267 getattr(h, "close_all", lambda : None)()
268
269 class _dummyremote(object):
270 """Dummy store storing blobs to temp directory."""
271
272 def __init__(self, repo, url):
273 fullpath = repo.vfs.join('lfs', url.path)
274 self.vfs = lfsvfs(fullpath)
275
276 def writebatch(self, pointers, fromstore):
277 for p in pointers:
278 content = fromstore.read(p.oid())
279 with self.vfs(p.oid(), 'wb', atomictemp=True) as fp:
280 fp.write(content)
281
282 def readbatch(self, pointers, tostore):
283 for p in pointers:
284 content = self.vfs.read(p.oid())
285 tostore.write(p.oid(), content)
286
287 class _nullremote(object):
288 """Null store storing blobs to /dev/null."""
289
290 def __init__(self, repo, url):
291 pass
292
293 def writebatch(self, pointers, fromstore):
294 pass
295
296 def readbatch(self, pointers, tostore):
297 pass
298
299 class _promptremote(object):
300 """Prompt user to set lfs.url when accessed."""
301
302 def __init__(self, repo, url):
303 pass
304
305 def writebatch(self, pointers, fromstore, ui=None):
306 self._prompt()
307
308 def readbatch(self, pointers, tostore, ui=None):
309 self._prompt()
310
311 def _prompt(self):
312 raise error.Abort(_('lfs.url needs to be configured'))
313
314 _storemap = {
315 'https': _gitlfsremote,
316 'http': _gitlfsremote,
317 'file': _dummyremote,
318 'null': _nullremote,
319 None: _promptremote,
320 }
321
322 def remote(repo):
323 """remotestore factory. return a store in _storemap depending on config"""
324 defaulturl = ''
325
326 # convert deprecated configs to the new url. TODO: remove this if other
327 # places are migrated to the new url config.
328 # deprecated config: lfs.remotestore
329 deprecatedstore = repo.ui.config('lfs', 'remotestore')
330 if deprecatedstore == 'dummy':
331 # deprecated config: lfs.remotepath
332 defaulturl = 'file://' + repo.ui.config('lfs', 'remotepath')
333 elif deprecatedstore == 'git-lfs':
334 # deprecated config: lfs.remoteurl
335 defaulturl = repo.ui.config('lfs', 'remoteurl')
336 elif deprecatedstore == 'null':
337 defaulturl = 'null://'
338
339 url = util.url(repo.ui.config('lfs', 'url', defaulturl))
340 scheme = url.scheme
341 if scheme not in _storemap:
342 raise error.Abort(_('lfs: unknown url scheme: %s') % scheme)
343 return _storemap[scheme](repo, url)
344
345 class LfsRemoteError(error.RevlogError):
346 pass
@@ -0,0 +1,72 b''
1 # pointer.py - Git-LFS pointer serialization
2 #
3 # Copyright 2017 Facebook, Inc.
4 #
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.
7
8 from __future__ import absolute_import
9
10 import re
11
12 from mercurial import (
13 error,
14 )
15 from mercurial.i18n import _
16
17 class InvalidPointer(error.RevlogError):
18 pass
19
20 class gitlfspointer(dict):
21 VERSION = 'https://git-lfs.github.com/spec/v1'
22
23 def __init__(self, *args, **kwargs):
24 self['version'] = self.VERSION
25 super(gitlfspointer, self).__init__(*args, **kwargs)
26
27 @classmethod
28 def deserialize(cls, text):
29 try:
30 return cls(l.split(' ', 1) for l in text.splitlines()).validate()
31 except ValueError: # l.split returns 1 item instead of 2
32 raise InvalidPointer(_('cannot parse git-lfs text: %r') % text)
33
34 def serialize(self):
35 sortkeyfunc = lambda x: (x[0] != 'version', x)
36 items = sorted(self.validate().iteritems(), key=sortkeyfunc)
37 return ''.join('%s %s\n' % (k, v) for k, v in items)
38
39 def oid(self):
40 return self['oid'].split(':')[-1]
41
42 def size(self):
43 return int(self['size'])
44
45 # regular expressions used by _validate
46 # see https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
47 _keyre = re.compile(r'\A[a-z0-9.-]+\Z')
48 _valuere = re.compile(r'\A[^\n]*\Z')
49 _requiredre = {
50 'size': re.compile(r'\A[0-9]+\Z'),
51 'oid': re.compile(r'\Asha256:[0-9a-f]{64}\Z'),
52 'version': re.compile(r'\A%s\Z' % re.escape(VERSION)),
53 }
54
55 def validate(self):
56 """raise InvalidPointer on error. return self if there is no error"""
57 requiredcount = 0
58 for k, v in self.iteritems():
59 if k in self._requiredre:
60 if not self._requiredre[k].match(v):
61 raise InvalidPointer(_('unexpected value: %s=%r') % (k, v))
62 requiredcount += 1
63 elif not self._keyre.match(k):
64 raise InvalidPointer(_('unexpected key: %s') % k)
65 if not self._valuere.match(v):
66 raise InvalidPointer(_('unexpected value: %s=%r') % (k, v))
67 if len(self._requiredre) != requiredcount:
68 miss = sorted(set(self._requiredre.keys()).difference(self.keys()))
69 raise InvalidPointer(_('missed keys: %s') % ', '.join(miss))
70 return self
71
72 deserialize = gitlfspointer.deserialize
@@ -0,0 +1,247 b''
1 # wrapper.py - methods wrapping core mercurial logic
2 #
3 # Copyright 2017 Facebook, Inc.
4 #
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.
7
8 from __future__ import absolute_import
9
10 import hashlib
11
12 from mercurial import (
13 error,
14 filelog,
15 revlog,
16 util,
17 )
18 from mercurial.i18n import _
19 from mercurial.node import bin, nullid, short
20
21 from . import (
22 blobstore,
23 pointer,
24 )
25
26 def supportedoutgoingversions(orig, repo):
27 versions = orig(repo)
28 versions.discard('01')
29 versions.discard('02')
30 versions.add('03')
31 return versions
32
33 def allsupportedversions(orig, ui):
34 versions = orig(ui)
35 versions.add('03')
36 return versions
37
38 def bypasscheckhash(self, text):
39 return False
40
41 def readfromstore(self, text):
42 """Read filelog content from local blobstore transform for flagprocessor.
43
44 Default tranform for flagprocessor, returning contents from blobstore.
45 Returns a 2-typle (text, validatehash) where validatehash is True as the
46 contents of the blobstore should be checked using checkhash.
47 """
48 p = pointer.deserialize(text)
49 oid = p.oid()
50 store = self.opener.lfslocalblobstore
51 if not store.has(oid):
52 p.filename = getattr(self, 'indexfile', None)
53 self.opener.lfsremoteblobstore.readbatch([p], store)
54 text = store.read(oid)
55
56 # pack hg filelog metadata
57 hgmeta = {}
58 for k in p.keys():
59 if k.startswith('x-hg-'):
60 name = k[len('x-hg-'):]
61 hgmeta[name] = p[k]
62 if hgmeta or text.startswith('\1\n'):
63 text = filelog.packmeta(hgmeta, text)
64
65 return (text, True)
66
67 def writetostore(self, text):
68 # hg filelog metadata (includes rename, etc)
69 hgmeta, offset = filelog.parsemeta(text)
70 if offset and offset > 0:
71 # lfs blob does not contain hg filelog metadata
72 text = text[offset:]
73
74 # git-lfs only supports sha256
75 oid = hashlib.sha256(text).hexdigest()
76 self.opener.lfslocalblobstore.write(oid, text)
77
78 # replace contents with metadata
79 longoid = 'sha256:%s' % oid
80 metadata = pointer.gitlfspointer(oid=longoid, size=str(len(text)))
81
82 # by default, we expect the content to be binary. however, LFS could also
83 # be used for non-binary content. add a special entry for non-binary data.
84 # this will be used by filectx.isbinary().
85 if not util.binary(text):
86 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
87 metadata['x-is-binary'] = '0'
88
89 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
90 if hgmeta is not None:
91 for k, v in hgmeta.iteritems():
92 metadata['x-hg-%s' % k] = v
93
94 rawtext = metadata.serialize()
95 return (rawtext, False)
96
97 def _islfs(rlog, node=None, rev=None):
98 if rev is None:
99 if node is None:
100 # both None - likely working copy content where node is not ready
101 return False
102 rev = rlog.rev(node)
103 else:
104 node = rlog.node(rev)
105 if node == nullid:
106 return False
107 flags = rlog.flags(rev)
108 return bool(flags & revlog.REVIDX_EXTSTORED)
109
110 def filelogaddrevision(orig, self, text, transaction, link, p1, p2,
111 cachedelta=None, node=None,
112 flags=revlog.REVIDX_DEFAULT_FLAGS, **kwds):
113 threshold = self.opener.options['lfsthreshold']
114 textlen = len(text)
115 # exclude hg rename meta from file size
116 meta, offset = filelog.parsemeta(text)
117 if offset:
118 textlen -= offset
119
120 if threshold and textlen > threshold:
121 flags |= revlog.REVIDX_EXTSTORED
122
123 return orig(self, text, transaction, link, p1, p2, cachedelta=cachedelta,
124 node=node, flags=flags, **kwds)
125
126 def filelogrenamed(orig, self, node):
127 if _islfs(self, node):
128 rawtext = self.revision(node, raw=True)
129 if not rawtext:
130 return False
131 metadata = pointer.deserialize(rawtext)
132 if 'x-hg-copy' in metadata and 'x-hg-copyrev' in metadata:
133 return metadata['x-hg-copy'], bin(metadata['x-hg-copyrev'])
134 else:
135 return False
136 return orig(self, node)
137
138 def filelogsize(orig, self, rev):
139 if _islfs(self, rev=rev):
140 # fast path: use lfs metadata to answer size
141 rawtext = self.revision(rev, raw=True)
142 metadata = pointer.deserialize(rawtext)
143 return int(metadata['size'])
144 return orig(self, rev)
145
146 def filectxcmp(orig, self, fctx):
147 """returns True if text is different than fctx"""
148 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
149 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
150 # fast path: check LFS oid
151 p1 = pointer.deserialize(self.rawdata())
152 p2 = pointer.deserialize(fctx.rawdata())
153 return p1.oid() != p2.oid()
154 return orig(self, fctx)
155
156 def filectxisbinary(orig, self):
157 if self.islfs():
158 # fast path: use lfs metadata to answer isbinary
159 metadata = pointer.deserialize(self.rawdata())
160 # if lfs metadata says nothing, assume it's binary by default
161 return bool(int(metadata.get('x-is-binary', 1)))
162 return orig(self)
163
164 def filectxislfs(self):
165 return _islfs(self.filelog(), self.filenode())
166
167 def vfsinit(orig, self, othervfs):
168 orig(self, othervfs)
169 # copy lfs related options
170 for k, v in othervfs.options.items():
171 if k.startswith('lfs'):
172 self.options[k] = v
173 # also copy lfs blobstores. note: this can run before reposetup, so lfs
174 # blobstore attributes are not always ready at this time.
175 for name in ['lfslocalblobstore', 'lfsremoteblobstore']:
176 if util.safehasattr(othervfs, name):
177 setattr(self, name, getattr(othervfs, name))
178
179 def _canskipupload(repo):
180 # if remotestore is a null store, upload is a no-op and can be skipped
181 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
182
183 def candownload(repo):
184 # if remotestore is a null store, downloads will lead to nothing
185 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
186
187 def uploadblobsfromrevs(repo, revs):
188 '''upload lfs blobs introduced by revs
189
190 Note: also used by other extensions e. g. infinitepush. avoid renaming.
191 '''
192 if _canskipupload(repo):
193 return
194 pointers = extractpointers(repo, revs)
195 uploadblobs(repo, pointers)
196
197 def prepush(pushop):
198 """Prepush hook.
199
200 Read through the revisions to push, looking for filelog entries that can be
201 deserialized into metadata so that we can block the push on their upload to
202 the remote blobstore.
203 """
204 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
205
206 def writenewbundle(orig, ui, repo, source, filename, bundletype, outgoing,
207 *args, **kwargs):
208 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
209 uploadblobsfromrevs(repo, outgoing.missing)
210 return orig(ui, repo, source, filename, bundletype, outgoing, *args,
211 **kwargs)
212
213 def extractpointers(repo, revs):
214 """return a list of lfs pointers added by given revs"""
215 ui = repo.ui
216 if ui.debugflag:
217 ui.write(_('lfs: computing set of blobs to upload\n'))
218 pointers = {}
219 for r in revs:
220 ctx = repo[r]
221 for p in pointersfromctx(ctx).values():
222 pointers[p.oid()] = p
223 return pointers.values()
224
225 def pointersfromctx(ctx):
226 """return a dict {path: pointer} for given single changectx"""
227 result = {}
228 for f in ctx.files():
229 if f not in ctx:
230 continue
231 fctx = ctx[f]
232 if not _islfs(fctx.filelog(), fctx.filenode()):
233 continue
234 try:
235 result[f] = pointer.deserialize(fctx.rawdata())
236 except pointer.InvalidPointer as ex:
237 raise error.Abort(_('lfs: corrupted pointer (%s@%s): %s\n')
238 % (f, short(ctx.node()), ex))
239 return result
240
241 def uploadblobs(repo, pointers):
242 """upload given pointers from local blobstore"""
243 if not pointers:
244 return
245
246 remoteblob = repo.svfs.lfsremoteblobstore
247 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
@@ -0,0 +1,41 b''
1 from __future__ import absolute_import, print_function
2
3 import os
4 import sys
5
6 # make it runnable using python directly without run-tests.py
7 sys.path[0:0] = [os.path.join(os.path.dirname(__file__), '..')]
8
9 from hgext.lfs import pointer
10
11 def tryparse(text):
12 r = {}
13 try:
14 r = pointer.deserialize(text)
15 print('ok')
16 except Exception as ex:
17 print(ex)
18 if r:
19 text2 = r.serialize()
20 if text2 != text:
21 print('reconstructed text differs')
22 return r
23
24 t = ('version https://git-lfs.github.com/spec/v1\n'
25 'oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1'
26 '258daaa5e2ca24d17e2393\n'
27 'size 12345\n'
28 'x-foo extra-information\n')
29
30 tryparse('')
31 tryparse(t)
32 tryparse(t.replace('git-lfs', 'unknown'))
33 tryparse(t.replace('v1\n', 'v1\n\n'))
34 tryparse(t.replace('sha256', 'ahs256'))
35 tryparse(t.replace('sha256:', ''))
36 tryparse(t.replace('12345', '0x12345'))
37 tryparse(t.replace('extra-information', 'extra\0information'))
38 tryparse(t.replace('extra-information', 'extra\ninformation'))
39 tryparse(t.replace('x-foo', 'x_foo'))
40 tryparse(t.replace('oid', 'blobid'))
41 tryparse(t.replace('size', 'size-bytes').replace('oid', 'object-id'))
@@ -0,0 +1,12 b''
1 missed keys: oid, size
2 ok
3 unexpected value: version='https://unknown.github.com/spec/v1'
4 cannot parse git-lfs text: 'version https://git-lfs.github.com/spec/v1\n\noid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\nsize 12345\nx-foo extra-information\n'
5 unexpected value: oid='ahs256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393'
6 unexpected value: oid='4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393'
7 unexpected value: size='0x12345'
8 ok
9 cannot parse git-lfs text: 'version https://git-lfs.github.com/spec/v1\noid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393\nsize 12345\nx-foo extra\ninformation\n'
10 unexpected key: x_foo
11 missed keys: oid
12 missed keys: oid, size
@@ -0,0 +1,108 b''
1 Require lfs-test-server (https://github.com/git-lfs/lfs-test-server)
2
3 $ hash lfs-test-server || { echo 'skipped: missing lfs-test-server'; exit 80; }
4
5 $ LFS_LISTEN="tcp://:$HGPORT"
6 $ LFS_HOST="localhost:$HGPORT"
7 $ LFS_PUBLIC=1
8 $ export LFS_LISTEN LFS_HOST LFS_PUBLIC
9 $ lfs-test-server &> lfs-server.log &
10 $ echo $! >> $DAEMON_PIDS
11
12 $ cat >> $HGRCPATH <<EOF
13 > [extensions]
14 > lfs=
15 > [lfs]
16 > url=http://foo:bar@$LFS_HOST/
17 > threshold=1
18 > EOF
19
20 $ hg init repo1
21 $ cd repo1
22 $ echo THIS-IS-LFS > a
23 $ hg commit -m a -A a
24
25 $ hg init ../repo2
26 $ hg push ../repo2 -v
27 pushing to ../repo2
28 searching for changes
29 lfs: uploading 31cf46fbc4ecd458a0943c5b4881f1f5a6dd36c53d6167d5b69ac45149b38e5b (12 bytes)
30 1 changesets found
31 uncompressed size of bundle content:
32 * (changelog) (glob)
33 * (manifests) (glob)
34 * a (glob)
35 adding changesets
36 adding manifests
37 adding file changes
38 added 1 changesets with 1 changes to 1 files
39
40 $ cd ../repo2
41 $ hg update tip -v
42 resolving manifests
43 getting a
44 lfs: downloading 31cf46fbc4ecd458a0943c5b4881f1f5a6dd36c53d6167d5b69ac45149b38e5b (12 bytes)
45 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
46
47 When the server has some blobs already
48
49 $ hg mv a b
50 $ echo ANOTHER-LARGE-FILE > c
51 $ echo ANOTHER-LARGE-FILE2 > d
52 $ hg commit -m b-and-c -A b c d
53 $ hg push ../repo1 -v | grep -v '^ '
54 pushing to ../repo1
55 searching for changes
56 lfs: need to transfer 2 objects (39 bytes)
57 lfs: uploading 37a65ab78d5ecda767e8622c248b5dbff1e68b1678ab0e730d5eb8601ec8ad19 (20 bytes)
58 lfs: uploading d11e1a642b60813aee592094109b406089b8dff4cb157157f753418ec7857998 (19 bytes)
59 1 changesets found
60 uncompressed size of bundle content:
61 adding changesets
62 adding manifests
63 adding file changes
64 added 1 changesets with 3 changes to 3 files
65
66 $ hg --repo ../repo1 update tip -v
67 resolving manifests
68 getting b
69 getting c
70 lfs: downloading d11e1a642b60813aee592094109b406089b8dff4cb157157f753418ec7857998 (19 bytes)
71 getting d
72 lfs: downloading 37a65ab78d5ecda767e8622c248b5dbff1e68b1678ab0e730d5eb8601ec8ad19 (20 bytes)
73 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
74
75 Check error message when the remote missed a blob:
76
77 $ echo FFFFF > b
78 $ hg commit -m b -A b
79 $ echo FFFFF >> b
80 $ hg commit -m b b
81 $ rm -rf .hg/store/lfs
82 $ hg update -C '.^'
83 abort: LFS server claims required objects do not exist:
84 8e6ea5f6c066b44a0efa43bcce86aea73f17e6e23f0663df0251e7524e140a13!
85 [255]
86
87 Check error message when object does not exist:
88
89 $ hg init test && cd test
90 $ echo "[extensions]" >> .hg/hgrc
91 $ echo "lfs=" >> .hg/hgrc
92 $ echo "[lfs]" >> .hg/hgrc
93 $ echo "threshold=1" >> .hg/hgrc
94 $ echo a > a
95 $ hg add a
96 $ hg commit -m 'test'
97 $ echo aaaaa > a
98 $ hg commit -m 'largefile'
99 $ hg debugdata .hg/store/data/a.i 1 # verify this is no the file content but includes "oid", the LFS "pointer".
100 version https://git-lfs.github.com/spec/v1
101 oid sha256:bdc26931acfb734b142a8d675f205becf27560dc461f501822de13274fe6fc8a
102 size 6
103 x-is-binary 0
104 $ cd ..
105 $ hg --config 'lfs.url=https://dewey-lfs.vip.facebook.com/lfs' clone test test2
106 updating to branch default
107 abort: LFS server error. Remote object for file data/a.i not found:(.*)! (re)
108 [255]
This diff has been collapsed as it changes many lines, (544 lines changed) Show them Hide them
@@ -0,0 +1,544 b''
1 # Initial setup
2
3 $ cat >> $HGRCPATH << EOF
4 > [extensions]
5 > lfs=
6 > [lfs]
7 > threshold=1000B
8 > EOF
9
10 $ LONG=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
11
12 # Prepare server and enable extension
13 $ hg init server
14 $ hg clone -q server client
15 $ cd client
16
17 # Commit small file
18 $ echo s > smallfile
19 $ hg commit -Aqm "add small file"
20
21 # Commit large file
22 $ echo $LONG > largefile
23 $ hg commit --traceback -Aqm "add large file"
24
25 # Ensure metadata is stored
26 $ hg debugdata largefile 0
27 version https://git-lfs.github.com/spec/v1
28 oid sha256:f11e77c257047a398492d8d6cb9f6acf3aa7c4384bb23080b43546053e183e4b
29 size 1501
30 x-is-binary 0
31
32 # Check the blobstore is populated
33 $ find .hg/store/lfs/objects | sort
34 .hg/store/lfs/objects
35 .hg/store/lfs/objects/f1
36 .hg/store/lfs/objects/f1/1e77c257047a398492d8d6cb9f6acf3aa7c4384bb23080b43546053e183e4b
37
38 # Check the blob stored contains the actual contents of the file
39 $ cat .hg/store/lfs/objects/f1/1e77c257047a398492d8d6cb9f6acf3aa7c4384bb23080b43546053e183e4b
40 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
41
42 # Push changes to the server
43
44 $ hg push
45 pushing to $TESTTMP/server (glob)
46 searching for changes
47 abort: lfs.url needs to be configured
48 [255]
49
50 $ cat >> $HGRCPATH << EOF
51 > [lfs]
52 > url=file:$TESTTMP/dummy-remote/
53 > EOF
54
55 $ hg push -v | egrep -v '^(uncompressed| )'
56 pushing to $TESTTMP/server (glob)
57 searching for changes
58 2 changesets found
59 adding changesets
60 adding manifests
61 adding file changes
62 added 2 changesets with 2 changes to 2 files
63
64 # Unknown URL scheme
65
66 $ hg push --config lfs.url=ftp://foobar
67 abort: lfs: unknown url scheme: ftp
68 [255]
69
70 $ cd ../
71
72 # Initialize new client (not cloning) and setup extension
73 $ hg init client2
74 $ cd client2
75 $ cat >> .hg/hgrc <<EOF
76 > [paths]
77 > default = $TESTTMP/server
78 > EOF
79
80 # Pull from server
81 $ hg pull default
82 pulling from $TESTTMP/server (glob)
83 requesting all changes
84 adding changesets
85 adding manifests
86 adding file changes
87 added 2 changesets with 2 changes to 2 files
88 new changesets b29ba743f89d:00c137947d30
89 (run 'hg update' to get a working copy)
90
91 # Check the blobstore is not yet populated
92 $ [ -d .hg/store/lfs/objects ]
93 [1]
94
95 # Update to the last revision containing the large file
96 $ hg update
97 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
98
99 # Check the blobstore has been populated on update
100 $ find .hg/store/lfs/objects | sort
101 .hg/store/lfs/objects
102 .hg/store/lfs/objects/f1
103 .hg/store/lfs/objects/f1/1e77c257047a398492d8d6cb9f6acf3aa7c4384bb23080b43546053e183e4b
104
105 # Check the contents of the file are fetched from blobstore when requested
106 $ hg cat -r . largefile
107 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
108
109 # Check the file has been copied in the working copy
110 $ cat largefile
111 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
112
113 $ cd ..
114
115 # Check rename, and switch between large and small files
116
117 $ hg init repo3
118 $ cd repo3
119 $ cat >> .hg/hgrc << EOF
120 > [lfs]
121 > threshold=10B
122 > EOF
123
124 $ echo LONGER-THAN-TEN-BYTES-WILL-TRIGGER-LFS > large
125 $ echo SHORTER > small
126 $ hg add . -q
127 $ hg commit -m 'commit with lfs content'
128
129 $ hg mv large l
130 $ hg mv small s
131 $ hg commit -m 'renames'
132
133 $ echo SHORT > l
134 $ echo BECOME-LARGER-FROM-SHORTER > s
135 $ hg commit -m 'large to small, small to large'
136
137 $ echo 1 >> l
138 $ echo 2 >> s
139 $ hg commit -m 'random modifications'
140
141 $ echo RESTORE-TO-BE-LARGE > l
142 $ echo SHORTER > s
143 $ hg commit -m 'switch large and small again'
144
145 # Test lfs_files template
146
147 $ hg log -r 'all()' -T '{rev} {join(lfs_files, ", ")}\n'
148 0 large
149 1 l
150 2 s
151 3 s
152 4 l
153
154 # Push and pull the above repo
155
156 $ hg --cwd .. init repo4
157 $ hg push ../repo4
158 pushing to ../repo4
159 searching for changes
160 adding changesets
161 adding manifests
162 adding file changes
163 added 5 changesets with 10 changes to 4 files
164
165 $ hg --cwd .. init repo5
166 $ hg --cwd ../repo5 pull ../repo3
167 pulling from ../repo3
168 requesting all changes
169 adding changesets
170 adding manifests
171 adding file changes
172 added 5 changesets with 10 changes to 4 files
173 new changesets fd47a419c4f7:5adf850972b9
174 (run 'hg update' to get a working copy)
175
176 $ cd ..
177
178 # Test clone
179
180 $ hg init repo6
181 $ cd repo6
182 $ cat >> .hg/hgrc << EOF
183 > [lfs]
184 > threshold=30B
185 > EOF
186
187 $ echo LARGE-BECAUSE-IT-IS-MORE-THAN-30-BYTES > large
188 $ echo SMALL > small
189 $ hg commit -Aqm 'create a lfs file' large small
190 $ hg debuglfsupload -r 'all()' -v
191
192 $ cd ..
193
194 $ hg clone repo6 repo7
195 updating to branch default
196 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
197 $ cd repo7
198 $ cat large
199 LARGE-BECAUSE-IT-IS-MORE-THAN-30-BYTES
200 $ cat small
201 SMALL
202
203 $ cd ..
204
205 # Test rename and status
206
207 $ hg init repo8
208 $ cd repo8
209 $ cat >> .hg/hgrc << EOF
210 > [lfs]
211 > threshold=10B
212 > EOF
213
214 $ echo THIS-IS-LFS-BECAUSE-10-BYTES > a1
215 $ echo SMALL > a2
216 $ hg commit -m a -A a1 a2
217 $ hg status
218 $ hg mv a1 b1
219 $ hg mv a2 a1
220 $ hg mv b1 a2
221 $ hg commit -m b
222 $ hg status
223 $ HEADER=$'\1\n'
224 $ printf '%sSTART-WITH-HG-FILELOG-METADATA' "$HEADER" > a2
225 $ printf '%sMETA\n' "$HEADER" > a1
226 $ hg commit -m meta
227 $ hg status
228 $ hg log -T '{rev}: {file_copies} | {file_dels} | {file_adds}\n'
229 2: | |
230 1: a1 (a2)a2 (a1) | |
231 0: | | a1 a2
232
233 $ for n in a1 a2; do
234 > for r in 0 1 2; do
235 > printf '\n%s @ %s\n' $n $r
236 > hg debugdata $n $r
237 > done
238 > done
239
240 a1 @ 0
241 version https://git-lfs.github.com/spec/v1
242 oid sha256:5bb8341bee63b3649f222b2215bde37322bea075a30575aa685d8f8d21c77024
243 size 29
244 x-is-binary 0
245
246 a1 @ 1
247 \x01 (esc)
248 copy: a2
249 copyrev: 50470ad23cf937b1f4b9f80bfe54df38e65b50d9
250 \x01 (esc)
251 SMALL
252
253 a1 @ 2
254 \x01 (esc)
255 \x01 (esc)
256 \x01 (esc)
257 META
258
259 a2 @ 0
260 SMALL
261
262 a2 @ 1
263 version https://git-lfs.github.com/spec/v1
264 oid sha256:5bb8341bee63b3649f222b2215bde37322bea075a30575aa685d8f8d21c77024
265 size 29
266 x-hg-copy a1
267 x-hg-copyrev be23af27908a582af43e5cda209a5a9b319de8d4
268 x-is-binary 0
269
270 a2 @ 2
271 version https://git-lfs.github.com/spec/v1
272 oid sha256:876dadc86a8542f9798048f2c47f51dbf8e4359aed883e8ec80c5db825f0d943
273 size 32
274 x-is-binary 0
275
276 # Verify commit hashes include rename metadata
277
278 $ hg log -T '{rev}:{node|short} {desc}\n'
279 2:0fae949de7fa meta
280 1:9cd6bdffdac0 b
281 0:7f96794915f7 a
282
283 $ cd ..
284
285 # Test bundle
286
287 $ hg init repo9
288 $ cd repo9
289 $ cat >> .hg/hgrc << EOF
290 > [lfs]
291 > threshold=10B
292 > [diff]
293 > git=1
294 > EOF
295
296 $ for i in 0 single two three 4; do
297 > echo 'THIS-IS-LFS-'$i > a
298 > hg commit -m a-$i -A a
299 > done
300
301 $ hg update 2 -q
302 $ echo 'THIS-IS-LFS-2-CHILD' > a
303 $ hg commit -m branching -q
304
305 $ hg bundle --base 1 bundle.hg -v
306 4 changesets found
307 uncompressed size of bundle content:
308 * (changelog) (glob)
309 * (manifests) (glob)
310 * a (glob)
311 $ hg --config extensions.strip= strip -r 2 --no-backup --force -q
312 $ hg -R bundle.hg log -p -T '{rev} {desc}\n' a
313 5 branching
314 diff --git a/a b/a
315 --- a/a
316 +++ b/a
317 @@ -1,1 +1,1 @@
318 -THIS-IS-LFS-two
319 +THIS-IS-LFS-2-CHILD
320
321 4 a-4
322 diff --git a/a b/a
323 --- a/a
324 +++ b/a
325 @@ -1,1 +1,1 @@
326 -THIS-IS-LFS-three
327 +THIS-IS-LFS-4
328
329 3 a-three
330 diff --git a/a b/a
331 --- a/a
332 +++ b/a
333 @@ -1,1 +1,1 @@
334 -THIS-IS-LFS-two
335 +THIS-IS-LFS-three
336
337 2 a-two
338 diff --git a/a b/a
339 --- a/a
340 +++ b/a
341 @@ -1,1 +1,1 @@
342 -THIS-IS-LFS-single
343 +THIS-IS-LFS-two
344
345 1 a-single
346 diff --git a/a b/a
347 --- a/a
348 +++ b/a
349 @@ -1,1 +1,1 @@
350 -THIS-IS-LFS-0
351 +THIS-IS-LFS-single
352
353 0 a-0
354 diff --git a/a b/a
355 new file mode 100644
356 --- /dev/null
357 +++ b/a
358 @@ -0,0 +1,1 @@
359 +THIS-IS-LFS-0
360
361 $ hg bundle -R bundle.hg --base 1 bundle-again.hg -q
362 $ hg -R bundle-again.hg log -p -T '{rev} {desc}\n' a
363 5 branching
364 diff --git a/a b/a
365 --- a/a
366 +++ b/a
367 @@ -1,1 +1,1 @@
368 -THIS-IS-LFS-two
369 +THIS-IS-LFS-2-CHILD
370
371 4 a-4
372 diff --git a/a b/a
373 --- a/a
374 +++ b/a
375 @@ -1,1 +1,1 @@
376 -THIS-IS-LFS-three
377 +THIS-IS-LFS-4
378
379 3 a-three
380 diff --git a/a b/a
381 --- a/a
382 +++ b/a
383 @@ -1,1 +1,1 @@
384 -THIS-IS-LFS-two
385 +THIS-IS-LFS-three
386
387 2 a-two
388 diff --git a/a b/a
389 --- a/a
390 +++ b/a
391 @@ -1,1 +1,1 @@
392 -THIS-IS-LFS-single
393 +THIS-IS-LFS-two
394
395 1 a-single
396 diff --git a/a b/a
397 --- a/a
398 +++ b/a
399 @@ -1,1 +1,1 @@
400 -THIS-IS-LFS-0
401 +THIS-IS-LFS-single
402
403 0 a-0
404 diff --git a/a b/a
405 new file mode 100644
406 --- /dev/null
407 +++ b/a
408 @@ -0,0 +1,1 @@
409 +THIS-IS-LFS-0
410
411 $ cd ..
412
413 # Test isbinary
414
415 $ hg init repo10
416 $ cd repo10
417 $ cat >> .hg/hgrc << EOF
418 > [extensions]
419 > lfs=
420 > [lfs]
421 > threshold=1
422 > EOF
423 $ $PYTHON <<'EOF'
424 > def write(path, content):
425 > with open(path, 'wb') as f:
426 > f.write(content)
427 > write('a', b'\0\0')
428 > write('b', b'\1\n')
429 > write('c', b'\1\n\0')
430 > write('d', b'xx')
431 > EOF
432 $ hg add a b c d
433 $ hg diff --stat
434 a | Bin
435 b | 1 +
436 c | Bin
437 d | 1 +
438 4 files changed, 2 insertions(+), 0 deletions(-)
439 $ hg commit -m binarytest
440 $ cat > $TESTTMP/dumpbinary.py << EOF
441 > def reposetup(ui, repo):
442 > for n in 'abcd':
443 > ui.write(('%s: binary=%s\n') % (n, repo['.'][n].isbinary()))
444 > EOF
445 $ hg --config extensions.dumpbinary=$TESTTMP/dumpbinary.py id --trace
446 a: binary=True
447 b: binary=False
448 c: binary=True
449 d: binary=False
450 b55353847f02 tip
451
452 $ cd ..
453
454 # Test fctx.cmp fastpath - diff without LFS blobs
455
456 $ hg init repo11
457 $ cd repo11
458 $ cat >> .hg/hgrc <<EOF
459 > [lfs]
460 > threshold=1
461 > EOF
462 $ for i in 1 2 3; do
463 > cp ../repo10/a a
464 > if [ $i = 3 ]; then
465 > # make a content-only change
466 > chmod +x a
467 > i=2
468 > fi
469 > echo $i >> a
470 > hg commit -m $i -A a
471 > done
472 $ [ -d .hg/store/lfs/objects ]
473
474 $ cd ..
475
476 $ hg clone repo11 repo12 --noupdate
477 $ cd repo12
478 $ hg log --removed -p a -T '{desc}\n' --config diff.nobinary=1 --git
479 2
480 diff --git a/a b/a
481 old mode 100644
482 new mode 100755
483
484 2
485 diff --git a/a b/a
486 Binary file a has changed
487
488 1
489 diff --git a/a b/a
490 new file mode 100644
491 Binary file a has changed
492
493 $ [ -d .hg/store/lfs/objects ]
494 [1]
495
496 $ cd ..
497
498 # Verify the repos
499
500 $ cat > $TESTTMP/dumpflog.py << EOF
501 > # print raw revision sizes, flags, and hashes for certain files
502 > import hashlib
503 > from mercurial import revlog
504 > from mercurial.node import short
505 > def hash(rawtext):
506 > h = hashlib.sha512()
507 > h.update(rawtext)
508 > return h.hexdigest()[:4]
509 > def reposetup(ui, repo):
510 > # these 2 files are interesting
511 > for name in ['l', 's']:
512 > fl = repo.file(name)
513 > if len(fl) == 0:
514 > continue
515 > sizes = [revlog.revlog.rawsize(fl, i) for i in fl]
516 > texts = [fl.revision(i, raw=True) for i in fl]
517 > flags = [fl.flags(i) for i in fl]
518 > hashes = [hash(t) for t in texts]
519 > print(' %s: rawsizes=%r flags=%r hashes=%r'
520 > % (name, sizes, flags, hashes))
521 > EOF
522
523 $ for i in client client2 server repo3 repo4 repo5 repo6 repo7 repo8 repo9 \
524 > repo10; do
525 > echo 'repo:' $i
526 > hg --cwd $i verify --config extensions.dumpflog=$TESTTMP/dumpflog.py -q
527 > done
528 repo: client
529 repo: client2
530 repo: server
531 repo: repo3
532 l: rawsizes=[211, 6, 8, 141] flags=[8192, 0, 0, 8192] hashes=['d2b8', '948c', 'cc88', '724d']
533 s: rawsizes=[74, 141, 141, 8] flags=[0, 8192, 8192, 0] hashes=['3c80', 'fce0', '874a', '826b']
534 repo: repo4
535 l: rawsizes=[211, 6, 8, 141] flags=[8192, 0, 0, 8192] hashes=['d2b8', '948c', 'cc88', '724d']
536 s: rawsizes=[74, 141, 141, 8] flags=[0, 8192, 8192, 0] hashes=['3c80', 'fce0', '874a', '826b']
537 repo: repo5
538 l: rawsizes=[211, 6, 8, 141] flags=[8192, 0, 0, 8192] hashes=['d2b8', '948c', 'cc88', '724d']
539 s: rawsizes=[74, 141, 141, 8] flags=[0, 8192, 8192, 0] hashes=['3c80', 'fce0', '874a', '826b']
540 repo: repo6
541 repo: repo7
542 repo: repo8
543 repo: repo9
544 repo: repo10
General Comments 0
You need to be logged in to leave comments. Login now