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