##// END OF EJS Templates
filelog: store filename directly on revlog instance...
Gregory Szorc -
r39892:96838b62 default
parent child Browse files
Show More
@@ -1,406 +1,406 b''
1 # wrapper.py - methods wrapping core mercurial logic
1 # wrapper.py - methods wrapping core mercurial logic
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
10 import hashlib
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial.node import bin, hex, nullid, short
13 from mercurial.node import bin, hex, nullid, short
14
14
15 from mercurial import (
15 from mercurial import (
16 error,
16 error,
17 repository,
17 repository,
18 revlog,
18 revlog,
19 util,
19 util,
20 )
20 )
21
21
22 from mercurial.utils import (
22 from mercurial.utils import (
23 stringutil,
23 stringutil,
24 )
24 )
25
25
26 from ..largefiles import lfutil
26 from ..largefiles import lfutil
27
27
28 from . import (
28 from . import (
29 blobstore,
29 blobstore,
30 pointer,
30 pointer,
31 )
31 )
32
32
33 def localrepomakefilestorage(orig, requirements, features, **kwargs):
33 def localrepomakefilestorage(orig, requirements, features, **kwargs):
34 if b'lfs' in requirements:
34 if b'lfs' in requirements:
35 features.add(repository.REPO_FEATURE_LFS)
35 features.add(repository.REPO_FEATURE_LFS)
36
36
37 return orig(requirements=requirements, features=features, **kwargs)
37 return orig(requirements=requirements, features=features, **kwargs)
38
38
39 def allsupportedversions(orig, ui):
39 def allsupportedversions(orig, ui):
40 versions = orig(ui)
40 versions = orig(ui)
41 versions.add('03')
41 versions.add('03')
42 return versions
42 return versions
43
43
44 def _capabilities(orig, repo, proto):
44 def _capabilities(orig, repo, proto):
45 '''Wrap server command to announce lfs server capability'''
45 '''Wrap server command to announce lfs server capability'''
46 caps = orig(repo, proto)
46 caps = orig(repo, proto)
47 if util.safehasattr(repo.svfs, 'lfslocalblobstore'):
47 if util.safehasattr(repo.svfs, 'lfslocalblobstore'):
48 # XXX: change to 'lfs=serve' when separate git server isn't required?
48 # XXX: change to 'lfs=serve' when separate git server isn't required?
49 caps.append('lfs')
49 caps.append('lfs')
50 return caps
50 return caps
51
51
52 def bypasscheckhash(self, text):
52 def bypasscheckhash(self, text):
53 return False
53 return False
54
54
55 def readfromstore(self, text):
55 def readfromstore(self, text):
56 """Read filelog content from local blobstore transform for flagprocessor.
56 """Read filelog content from local blobstore transform for flagprocessor.
57
57
58 Default tranform for flagprocessor, returning contents from blobstore.
58 Default tranform for flagprocessor, returning contents from blobstore.
59 Returns a 2-typle (text, validatehash) where validatehash is True as the
59 Returns a 2-typle (text, validatehash) where validatehash is True as the
60 contents of the blobstore should be checked using checkhash.
60 contents of the blobstore should be checked using checkhash.
61 """
61 """
62 p = pointer.deserialize(text)
62 p = pointer.deserialize(text)
63 oid = p.oid()
63 oid = p.oid()
64 store = self.opener.lfslocalblobstore
64 store = self.opener.lfslocalblobstore
65 if not store.has(oid):
65 if not store.has(oid):
66 p.filename = self.filename
66 p.filename = self.filename
67 self.opener.lfsremoteblobstore.readbatch([p], store)
67 self.opener.lfsremoteblobstore.readbatch([p], store)
68
68
69 # The caller will validate the content
69 # The caller will validate the content
70 text = store.read(oid, verify=False)
70 text = store.read(oid, verify=False)
71
71
72 # pack hg filelog metadata
72 # pack hg filelog metadata
73 hgmeta = {}
73 hgmeta = {}
74 for k in p.keys():
74 for k in p.keys():
75 if k.startswith('x-hg-'):
75 if k.startswith('x-hg-'):
76 name = k[len('x-hg-'):]
76 name = k[len('x-hg-'):]
77 hgmeta[name] = p[k]
77 hgmeta[name] = p[k]
78 if hgmeta or text.startswith('\1\n'):
78 if hgmeta or text.startswith('\1\n'):
79 text = revlog.packmeta(hgmeta, text)
79 text = revlog.packmeta(hgmeta, text)
80
80
81 return (text, True)
81 return (text, True)
82
82
83 def writetostore(self, text):
83 def writetostore(self, text):
84 # hg filelog metadata (includes rename, etc)
84 # hg filelog metadata (includes rename, etc)
85 hgmeta, offset = revlog.parsemeta(text)
85 hgmeta, offset = revlog.parsemeta(text)
86 if offset and offset > 0:
86 if offset and offset > 0:
87 # lfs blob does not contain hg filelog metadata
87 # lfs blob does not contain hg filelog metadata
88 text = text[offset:]
88 text = text[offset:]
89
89
90 # git-lfs only supports sha256
90 # git-lfs only supports sha256
91 oid = hex(hashlib.sha256(text).digest())
91 oid = hex(hashlib.sha256(text).digest())
92 self.opener.lfslocalblobstore.write(oid, text)
92 self.opener.lfslocalblobstore.write(oid, text)
93
93
94 # replace contents with metadata
94 # replace contents with metadata
95 longoid = 'sha256:%s' % oid
95 longoid = 'sha256:%s' % oid
96 metadata = pointer.gitlfspointer(oid=longoid, size='%d' % len(text))
96 metadata = pointer.gitlfspointer(oid=longoid, size='%d' % len(text))
97
97
98 # by default, we expect the content to be binary. however, LFS could also
98 # by default, we expect the content to be binary. however, LFS could also
99 # be used for non-binary content. add a special entry for non-binary data.
99 # be used for non-binary content. add a special entry for non-binary data.
100 # this will be used by filectx.isbinary().
100 # this will be used by filectx.isbinary().
101 if not stringutil.binary(text):
101 if not stringutil.binary(text):
102 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
102 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
103 metadata['x-is-binary'] = '0'
103 metadata['x-is-binary'] = '0'
104
104
105 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
105 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
106 if hgmeta is not None:
106 if hgmeta is not None:
107 for k, v in hgmeta.iteritems():
107 for k, v in hgmeta.iteritems():
108 metadata['x-hg-%s' % k] = v
108 metadata['x-hg-%s' % k] = v
109
109
110 rawtext = metadata.serialize()
110 rawtext = metadata.serialize()
111 return (rawtext, False)
111 return (rawtext, False)
112
112
113 def _islfs(rlog, node=None, rev=None):
113 def _islfs(rlog, node=None, rev=None):
114 if rev is None:
114 if rev is None:
115 if node is None:
115 if node is None:
116 # both None - likely working copy content where node is not ready
116 # both None - likely working copy content where node is not ready
117 return False
117 return False
118 rev = rlog._revlog.rev(node)
118 rev = rlog._revlog.rev(node)
119 else:
119 else:
120 node = rlog._revlog.node(rev)
120 node = rlog._revlog.node(rev)
121 if node == nullid:
121 if node == nullid:
122 return False
122 return False
123 flags = rlog._revlog.flags(rev)
123 flags = rlog._revlog.flags(rev)
124 return bool(flags & revlog.REVIDX_EXTSTORED)
124 return bool(flags & revlog.REVIDX_EXTSTORED)
125
125
126 def filelogaddrevision(orig, self, text, transaction, link, p1, p2,
126 def filelogaddrevision(orig, self, text, transaction, link, p1, p2,
127 cachedelta=None, node=None,
127 cachedelta=None, node=None,
128 flags=revlog.REVIDX_DEFAULT_FLAGS, **kwds):
128 flags=revlog.REVIDX_DEFAULT_FLAGS, **kwds):
129 # The matcher isn't available if reposetup() wasn't called.
129 # The matcher isn't available if reposetup() wasn't called.
130 lfstrack = self._revlog.opener.options.get('lfstrack')
130 lfstrack = self._revlog.opener.options.get('lfstrack')
131
131
132 if lfstrack:
132 if lfstrack:
133 textlen = len(text)
133 textlen = len(text)
134 # exclude hg rename meta from file size
134 # exclude hg rename meta from file size
135 meta, offset = revlog.parsemeta(text)
135 meta, offset = revlog.parsemeta(text)
136 if offset:
136 if offset:
137 textlen -= offset
137 textlen -= offset
138
138
139 if lfstrack(self.filename, textlen):
139 if lfstrack(self._revlog.filename, textlen):
140 flags |= revlog.REVIDX_EXTSTORED
140 flags |= revlog.REVIDX_EXTSTORED
141
141
142 return orig(self, text, transaction, link, p1, p2, cachedelta=cachedelta,
142 return orig(self, text, transaction, link, p1, p2, cachedelta=cachedelta,
143 node=node, flags=flags, **kwds)
143 node=node, flags=flags, **kwds)
144
144
145 def filelogrenamed(orig, self, node):
145 def filelogrenamed(orig, self, node):
146 if _islfs(self, node):
146 if _islfs(self, node):
147 rawtext = self._revlog.revision(node, raw=True)
147 rawtext = self._revlog.revision(node, raw=True)
148 if not rawtext:
148 if not rawtext:
149 return False
149 return False
150 metadata = pointer.deserialize(rawtext)
150 metadata = pointer.deserialize(rawtext)
151 if 'x-hg-copy' in metadata and 'x-hg-copyrev' in metadata:
151 if 'x-hg-copy' in metadata and 'x-hg-copyrev' in metadata:
152 return metadata['x-hg-copy'], bin(metadata['x-hg-copyrev'])
152 return metadata['x-hg-copy'], bin(metadata['x-hg-copyrev'])
153 else:
153 else:
154 return False
154 return False
155 return orig(self, node)
155 return orig(self, node)
156
156
157 def filelogsize(orig, self, rev):
157 def filelogsize(orig, self, rev):
158 if _islfs(self, rev=rev):
158 if _islfs(self, rev=rev):
159 # fast path: use lfs metadata to answer size
159 # fast path: use lfs metadata to answer size
160 rawtext = self._revlog.revision(rev, raw=True)
160 rawtext = self._revlog.revision(rev, raw=True)
161 metadata = pointer.deserialize(rawtext)
161 metadata = pointer.deserialize(rawtext)
162 return int(metadata['size'])
162 return int(metadata['size'])
163 return orig(self, rev)
163 return orig(self, rev)
164
164
165 def filectxcmp(orig, self, fctx):
165 def filectxcmp(orig, self, fctx):
166 """returns True if text is different than fctx"""
166 """returns True if text is different than fctx"""
167 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
167 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
168 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
168 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
169 # fast path: check LFS oid
169 # fast path: check LFS oid
170 p1 = pointer.deserialize(self.rawdata())
170 p1 = pointer.deserialize(self.rawdata())
171 p2 = pointer.deserialize(fctx.rawdata())
171 p2 = pointer.deserialize(fctx.rawdata())
172 return p1.oid() != p2.oid()
172 return p1.oid() != p2.oid()
173 return orig(self, fctx)
173 return orig(self, fctx)
174
174
175 def filectxisbinary(orig, self):
175 def filectxisbinary(orig, self):
176 if self.islfs():
176 if self.islfs():
177 # fast path: use lfs metadata to answer isbinary
177 # fast path: use lfs metadata to answer isbinary
178 metadata = pointer.deserialize(self.rawdata())
178 metadata = pointer.deserialize(self.rawdata())
179 # if lfs metadata says nothing, assume it's binary by default
179 # if lfs metadata says nothing, assume it's binary by default
180 return bool(int(metadata.get('x-is-binary', 1)))
180 return bool(int(metadata.get('x-is-binary', 1)))
181 return orig(self)
181 return orig(self)
182
182
183 def filectxislfs(self):
183 def filectxislfs(self):
184 return _islfs(self.filelog(), self.filenode())
184 return _islfs(self.filelog(), self.filenode())
185
185
186 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
186 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
187 orig(fm, ctx, matcher, path, decode)
187 orig(fm, ctx, matcher, path, decode)
188 fm.data(rawdata=ctx[path].rawdata())
188 fm.data(rawdata=ctx[path].rawdata())
189
189
190 def convertsink(orig, sink):
190 def convertsink(orig, sink):
191 sink = orig(sink)
191 sink = orig(sink)
192 if sink.repotype == 'hg':
192 if sink.repotype == 'hg':
193 class lfssink(sink.__class__):
193 class lfssink(sink.__class__):
194 def putcommit(self, files, copies, parents, commit, source, revmap,
194 def putcommit(self, files, copies, parents, commit, source, revmap,
195 full, cleanp2):
195 full, cleanp2):
196 pc = super(lfssink, self).putcommit
196 pc = super(lfssink, self).putcommit
197 node = pc(files, copies, parents, commit, source, revmap, full,
197 node = pc(files, copies, parents, commit, source, revmap, full,
198 cleanp2)
198 cleanp2)
199
199
200 if 'lfs' not in self.repo.requirements:
200 if 'lfs' not in self.repo.requirements:
201 ctx = self.repo[node]
201 ctx = self.repo[node]
202
202
203 # The file list may contain removed files, so check for
203 # The file list may contain removed files, so check for
204 # membership before assuming it is in the context.
204 # membership before assuming it is in the context.
205 if any(f in ctx and ctx[f].islfs() for f, n in files):
205 if any(f in ctx and ctx[f].islfs() for f, n in files):
206 self.repo.requirements.add('lfs')
206 self.repo.requirements.add('lfs')
207 self.repo._writerequirements()
207 self.repo._writerequirements()
208
208
209 # Permanently enable lfs locally
209 # Permanently enable lfs locally
210 self.repo.vfs.append(
210 self.repo.vfs.append(
211 'hgrc', util.tonativeeol('\n[extensions]\nlfs=\n'))
211 'hgrc', util.tonativeeol('\n[extensions]\nlfs=\n'))
212
212
213 return node
213 return node
214
214
215 sink.__class__ = lfssink
215 sink.__class__ = lfssink
216
216
217 return sink
217 return sink
218
218
219 def vfsinit(orig, self, othervfs):
219 def vfsinit(orig, self, othervfs):
220 orig(self, othervfs)
220 orig(self, othervfs)
221 # copy lfs related options
221 # copy lfs related options
222 for k, v in othervfs.options.items():
222 for k, v in othervfs.options.items():
223 if k.startswith('lfs'):
223 if k.startswith('lfs'):
224 self.options[k] = v
224 self.options[k] = v
225 # also copy lfs blobstores. note: this can run before reposetup, so lfs
225 # also copy lfs blobstores. note: this can run before reposetup, so lfs
226 # blobstore attributes are not always ready at this time.
226 # blobstore attributes are not always ready at this time.
227 for name in ['lfslocalblobstore', 'lfsremoteblobstore']:
227 for name in ['lfslocalblobstore', 'lfsremoteblobstore']:
228 if util.safehasattr(othervfs, name):
228 if util.safehasattr(othervfs, name):
229 setattr(self, name, getattr(othervfs, name))
229 setattr(self, name, getattr(othervfs, name))
230
230
231 def _prefetchfiles(repo, revs, match):
231 def _prefetchfiles(repo, revs, match):
232 """Ensure that required LFS blobs are present, fetching them as a group if
232 """Ensure that required LFS blobs are present, fetching them as a group if
233 needed."""
233 needed."""
234 if not util.safehasattr(repo.svfs, 'lfslocalblobstore'):
234 if not util.safehasattr(repo.svfs, 'lfslocalblobstore'):
235 return
235 return
236
236
237 pointers = []
237 pointers = []
238 oids = set()
238 oids = set()
239 localstore = repo.svfs.lfslocalblobstore
239 localstore = repo.svfs.lfslocalblobstore
240
240
241 for rev in revs:
241 for rev in revs:
242 ctx = repo[rev]
242 ctx = repo[rev]
243 for f in ctx.walk(match):
243 for f in ctx.walk(match):
244 p = pointerfromctx(ctx, f)
244 p = pointerfromctx(ctx, f)
245 if p and p.oid() not in oids and not localstore.has(p.oid()):
245 if p and p.oid() not in oids and not localstore.has(p.oid()):
246 p.filename = f
246 p.filename = f
247 pointers.append(p)
247 pointers.append(p)
248 oids.add(p.oid())
248 oids.add(p.oid())
249
249
250 if pointers:
250 if pointers:
251 # Recalculating the repo store here allows 'paths.default' that is set
251 # Recalculating the repo store here allows 'paths.default' that is set
252 # on the repo by a clone command to be used for the update.
252 # on the repo by a clone command to be used for the update.
253 blobstore.remote(repo).readbatch(pointers, localstore)
253 blobstore.remote(repo).readbatch(pointers, localstore)
254
254
255 def _canskipupload(repo):
255 def _canskipupload(repo):
256 # Skip if this hasn't been passed to reposetup()
256 # Skip if this hasn't been passed to reposetup()
257 if not util.safehasattr(repo.svfs, 'lfsremoteblobstore'):
257 if not util.safehasattr(repo.svfs, 'lfsremoteblobstore'):
258 return True
258 return True
259
259
260 # if remotestore is a null store, upload is a no-op and can be skipped
260 # if remotestore is a null store, upload is a no-op and can be skipped
261 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
261 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
262
262
263 def candownload(repo):
263 def candownload(repo):
264 # Skip if this hasn't been passed to reposetup()
264 # Skip if this hasn't been passed to reposetup()
265 if not util.safehasattr(repo.svfs, 'lfsremoteblobstore'):
265 if not util.safehasattr(repo.svfs, 'lfsremoteblobstore'):
266 return False
266 return False
267
267
268 # if remotestore is a null store, downloads will lead to nothing
268 # if remotestore is a null store, downloads will lead to nothing
269 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
269 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
270
270
271 def uploadblobsfromrevs(repo, revs):
271 def uploadblobsfromrevs(repo, revs):
272 '''upload lfs blobs introduced by revs
272 '''upload lfs blobs introduced by revs
273
273
274 Note: also used by other extensions e. g. infinitepush. avoid renaming.
274 Note: also used by other extensions e. g. infinitepush. avoid renaming.
275 '''
275 '''
276 if _canskipupload(repo):
276 if _canskipupload(repo):
277 return
277 return
278 pointers = extractpointers(repo, revs)
278 pointers = extractpointers(repo, revs)
279 uploadblobs(repo, pointers)
279 uploadblobs(repo, pointers)
280
280
281 def prepush(pushop):
281 def prepush(pushop):
282 """Prepush hook.
282 """Prepush hook.
283
283
284 Read through the revisions to push, looking for filelog entries that can be
284 Read through the revisions to push, looking for filelog entries that can be
285 deserialized into metadata so that we can block the push on their upload to
285 deserialized into metadata so that we can block the push on their upload to
286 the remote blobstore.
286 the remote blobstore.
287 """
287 """
288 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
288 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
289
289
290 def push(orig, repo, remote, *args, **kwargs):
290 def push(orig, repo, remote, *args, **kwargs):
291 """bail on push if the extension isn't enabled on remote when needed, and
291 """bail on push if the extension isn't enabled on remote when needed, and
292 update the remote store based on the destination path."""
292 update the remote store based on the destination path."""
293 if 'lfs' in repo.requirements:
293 if 'lfs' in repo.requirements:
294 # If the remote peer is for a local repo, the requirement tests in the
294 # If the remote peer is for a local repo, the requirement tests in the
295 # base class method enforce lfs support. Otherwise, some revisions in
295 # base class method enforce lfs support. Otherwise, some revisions in
296 # this repo use lfs, and the remote repo needs the extension loaded.
296 # this repo use lfs, and the remote repo needs the extension loaded.
297 if not remote.local() and not remote.capable('lfs'):
297 if not remote.local() and not remote.capable('lfs'):
298 # This is a copy of the message in exchange.push() when requirements
298 # This is a copy of the message in exchange.push() when requirements
299 # are missing between local repos.
299 # are missing between local repos.
300 m = _("required features are not supported in the destination: %s")
300 m = _("required features are not supported in the destination: %s")
301 raise error.Abort(m % 'lfs',
301 raise error.Abort(m % 'lfs',
302 hint=_('enable the lfs extension on the server'))
302 hint=_('enable the lfs extension on the server'))
303
303
304 # Repositories where this extension is disabled won't have the field.
304 # Repositories where this extension is disabled won't have the field.
305 # But if there's a requirement, then the extension must be loaded AND
305 # But if there's a requirement, then the extension must be loaded AND
306 # there may be blobs to push.
306 # there may be blobs to push.
307 remotestore = repo.svfs.lfsremoteblobstore
307 remotestore = repo.svfs.lfsremoteblobstore
308 try:
308 try:
309 repo.svfs.lfsremoteblobstore = blobstore.remote(repo, remote.url())
309 repo.svfs.lfsremoteblobstore = blobstore.remote(repo, remote.url())
310 return orig(repo, remote, *args, **kwargs)
310 return orig(repo, remote, *args, **kwargs)
311 finally:
311 finally:
312 repo.svfs.lfsremoteblobstore = remotestore
312 repo.svfs.lfsremoteblobstore = remotestore
313 else:
313 else:
314 return orig(repo, remote, *args, **kwargs)
314 return orig(repo, remote, *args, **kwargs)
315
315
316 def writenewbundle(orig, ui, repo, source, filename, bundletype, outgoing,
316 def writenewbundle(orig, ui, repo, source, filename, bundletype, outgoing,
317 *args, **kwargs):
317 *args, **kwargs):
318 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
318 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
319 uploadblobsfromrevs(repo, outgoing.missing)
319 uploadblobsfromrevs(repo, outgoing.missing)
320 return orig(ui, repo, source, filename, bundletype, outgoing, *args,
320 return orig(ui, repo, source, filename, bundletype, outgoing, *args,
321 **kwargs)
321 **kwargs)
322
322
323 def extractpointers(repo, revs):
323 def extractpointers(repo, revs):
324 """return a list of lfs pointers added by given revs"""
324 """return a list of lfs pointers added by given revs"""
325 repo.ui.debug('lfs: computing set of blobs to upload\n')
325 repo.ui.debug('lfs: computing set of blobs to upload\n')
326 pointers = {}
326 pointers = {}
327
327
328 makeprogress = repo.ui.makeprogress
328 makeprogress = repo.ui.makeprogress
329 with makeprogress(_('lfs search'), _('changesets'), len(revs)) as progress:
329 with makeprogress(_('lfs search'), _('changesets'), len(revs)) as progress:
330 for r in revs:
330 for r in revs:
331 ctx = repo[r]
331 ctx = repo[r]
332 for p in pointersfromctx(ctx).values():
332 for p in pointersfromctx(ctx).values():
333 pointers[p.oid()] = p
333 pointers[p.oid()] = p
334 progress.increment()
334 progress.increment()
335 return sorted(pointers.values())
335 return sorted(pointers.values())
336
336
337 def pointerfromctx(ctx, f, removed=False):
337 def pointerfromctx(ctx, f, removed=False):
338 """return a pointer for the named file from the given changectx, or None if
338 """return a pointer for the named file from the given changectx, or None if
339 the file isn't LFS.
339 the file isn't LFS.
340
340
341 Optionally, the pointer for a file deleted from the context can be returned.
341 Optionally, the pointer for a file deleted from the context can be returned.
342 Since no such pointer is actually stored, and to distinguish from a non LFS
342 Since no such pointer is actually stored, and to distinguish from a non LFS
343 file, this pointer is represented by an empty dict.
343 file, this pointer is represented by an empty dict.
344 """
344 """
345 _ctx = ctx
345 _ctx = ctx
346 if f not in ctx:
346 if f not in ctx:
347 if not removed:
347 if not removed:
348 return None
348 return None
349 if f in ctx.p1():
349 if f in ctx.p1():
350 _ctx = ctx.p1()
350 _ctx = ctx.p1()
351 elif f in ctx.p2():
351 elif f in ctx.p2():
352 _ctx = ctx.p2()
352 _ctx = ctx.p2()
353 else:
353 else:
354 return None
354 return None
355 fctx = _ctx[f]
355 fctx = _ctx[f]
356 if not _islfs(fctx.filelog(), fctx.filenode()):
356 if not _islfs(fctx.filelog(), fctx.filenode()):
357 return None
357 return None
358 try:
358 try:
359 p = pointer.deserialize(fctx.rawdata())
359 p = pointer.deserialize(fctx.rawdata())
360 if ctx == _ctx:
360 if ctx == _ctx:
361 return p
361 return p
362 return {}
362 return {}
363 except pointer.InvalidPointer as ex:
363 except pointer.InvalidPointer as ex:
364 raise error.Abort(_('lfs: corrupted pointer (%s@%s): %s\n')
364 raise error.Abort(_('lfs: corrupted pointer (%s@%s): %s\n')
365 % (f, short(_ctx.node()), ex))
365 % (f, short(_ctx.node()), ex))
366
366
367 def pointersfromctx(ctx, removed=False):
367 def pointersfromctx(ctx, removed=False):
368 """return a dict {path: pointer} for given single changectx.
368 """return a dict {path: pointer} for given single changectx.
369
369
370 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
370 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
371 stored for the path is an empty dict.
371 stored for the path is an empty dict.
372 """
372 """
373 result = {}
373 result = {}
374 for f in ctx.files():
374 for f in ctx.files():
375 p = pointerfromctx(ctx, f, removed=removed)
375 p = pointerfromctx(ctx, f, removed=removed)
376 if p is not None:
376 if p is not None:
377 result[f] = p
377 result[f] = p
378 return result
378 return result
379
379
380 def uploadblobs(repo, pointers):
380 def uploadblobs(repo, pointers):
381 """upload given pointers from local blobstore"""
381 """upload given pointers from local blobstore"""
382 if not pointers:
382 if not pointers:
383 return
383 return
384
384
385 remoteblob = repo.svfs.lfsremoteblobstore
385 remoteblob = repo.svfs.lfsremoteblobstore
386 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
386 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
387
387
388 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
388 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
389 orig(ui, srcrepo, dstrepo, requirements)
389 orig(ui, srcrepo, dstrepo, requirements)
390
390
391 # Skip if this hasn't been passed to reposetup()
391 # Skip if this hasn't been passed to reposetup()
392 if (util.safehasattr(srcrepo.svfs, 'lfslocalblobstore') and
392 if (util.safehasattr(srcrepo.svfs, 'lfslocalblobstore') and
393 util.safehasattr(dstrepo.svfs, 'lfslocalblobstore')):
393 util.safehasattr(dstrepo.svfs, 'lfslocalblobstore')):
394 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
394 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
395 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
395 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
396
396
397 for dirpath, dirs, files in srclfsvfs.walk():
397 for dirpath, dirs, files in srclfsvfs.walk():
398 for oid in files:
398 for oid in files:
399 ui.write(_('copying lfs blob %s\n') % oid)
399 ui.write(_('copying lfs blob %s\n') % oid)
400 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
400 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
401
401
402 def upgraderequirements(orig, repo):
402 def upgraderequirements(orig, repo):
403 reqs = orig(repo)
403 reqs = orig(repo)
404 if 'lfs' in repo.requirements:
404 if 'lfs' in repo.requirements:
405 reqs.add('lfs')
405 reqs.add('lfs')
406 return reqs
406 return reqs
@@ -1,271 +1,262 b''
1 # filelog.py - file history class for mercurial
1 # filelog.py - file history class for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from . import (
10 from . import (
11 error,
11 error,
12 repository,
12 repository,
13 revlog,
13 revlog,
14 )
14 )
15 from .utils import (
15 from .utils import (
16 interfaceutil,
16 interfaceutil,
17 )
17 )
18
18
19 @interfaceutil.implementer(repository.ifilestorage)
19 @interfaceutil.implementer(repository.ifilestorage)
20 class filelog(object):
20 class filelog(object):
21 def __init__(self, opener, path):
21 def __init__(self, opener, path):
22 self._revlog = revlog.revlog(opener,
22 self._revlog = revlog.revlog(opener,
23 '/'.join(('data', path + '.i')),
23 '/'.join(('data', path + '.i')),
24 censorable=True)
24 censorable=True)
25 # Full name of the user visible file, relative to the repository root.
25 # Full name of the user visible file, relative to the repository root.
26 # Used by LFS.
26 # Used by LFS.
27 self.filename = path
27 self._revlog.filename = path
28 # Used by repo upgrade.
28 # Used by repo upgrade.
29 self.index = self._revlog.index
29 self.index = self._revlog.index
30 # Used by changegroup generation.
30 # Used by changegroup generation.
31 self._generaldelta = self._revlog._generaldelta
31 self._generaldelta = self._revlog._generaldelta
32
32
33 def __len__(self):
33 def __len__(self):
34 return len(self._revlog)
34 return len(self._revlog)
35
35
36 def __iter__(self):
36 def __iter__(self):
37 return self._revlog.__iter__()
37 return self._revlog.__iter__()
38
38
39 def revs(self, start=0, stop=None):
39 def revs(self, start=0, stop=None):
40 return self._revlog.revs(start=start, stop=stop)
40 return self._revlog.revs(start=start, stop=stop)
41
41
42 def parents(self, node):
42 def parents(self, node):
43 return self._revlog.parents(node)
43 return self._revlog.parents(node)
44
44
45 def parentrevs(self, rev):
45 def parentrevs(self, rev):
46 return self._revlog.parentrevs(rev)
46 return self._revlog.parentrevs(rev)
47
47
48 def rev(self, node):
48 def rev(self, node):
49 return self._revlog.rev(node)
49 return self._revlog.rev(node)
50
50
51 def node(self, rev):
51 def node(self, rev):
52 return self._revlog.node(rev)
52 return self._revlog.node(rev)
53
53
54 def lookup(self, node):
54 def lookup(self, node):
55 return self._revlog.lookup(node)
55 return self._revlog.lookup(node)
56
56
57 def linkrev(self, rev):
57 def linkrev(self, rev):
58 return self._revlog.linkrev(rev)
58 return self._revlog.linkrev(rev)
59
59
60 # Used by verify.
60 # Used by verify.
61 def flags(self, rev):
61 def flags(self, rev):
62 return self._revlog.flags(rev)
62 return self._revlog.flags(rev)
63
63
64 def commonancestorsheads(self, node1, node2):
64 def commonancestorsheads(self, node1, node2):
65 return self._revlog.commonancestorsheads(node1, node2)
65 return self._revlog.commonancestorsheads(node1, node2)
66
66
67 # Used by dagop.blockdescendants().
67 # Used by dagop.blockdescendants().
68 def descendants(self, revs):
68 def descendants(self, revs):
69 return self._revlog.descendants(revs)
69 return self._revlog.descendants(revs)
70
70
71 def heads(self, start=None, stop=None):
71 def heads(self, start=None, stop=None):
72 return self._revlog.heads(start, stop)
72 return self._revlog.heads(start, stop)
73
73
74 # Used by hgweb, children extension.
74 # Used by hgweb, children extension.
75 def children(self, node):
75 def children(self, node):
76 return self._revlog.children(node)
76 return self._revlog.children(node)
77
77
78 def deltaparent(self, rev):
78 def deltaparent(self, rev):
79 return self._revlog.deltaparent(rev)
79 return self._revlog.deltaparent(rev)
80
80
81 def iscensored(self, rev):
81 def iscensored(self, rev):
82 return self._revlog.iscensored(rev)
82 return self._revlog.iscensored(rev)
83
83
84 # Used by verify.
84 # Used by verify.
85 def rawsize(self, rev):
85 def rawsize(self, rev):
86 return self._revlog.rawsize(rev)
86 return self._revlog.rawsize(rev)
87
87
88 # Might be unused.
88 # Might be unused.
89 def checkhash(self, text, node, p1=None, p2=None, rev=None):
89 def checkhash(self, text, node, p1=None, p2=None, rev=None):
90 return self._revlog.checkhash(text, node, p1=p1, p2=p2, rev=rev)
90 return self._revlog.checkhash(text, node, p1=p1, p2=p2, rev=rev)
91
91
92 def revision(self, node, _df=None, raw=False):
92 def revision(self, node, _df=None, raw=False):
93 return self._revlog.revision(node, _df=_df, raw=raw)
93 return self._revlog.revision(node, _df=_df, raw=raw)
94
94
95 def revdiff(self, rev1, rev2):
95 def revdiff(self, rev1, rev2):
96 return self._revlog.revdiff(rev1, rev2)
96 return self._revlog.revdiff(rev1, rev2)
97
97
98 def emitrevisiondeltas(self, requests):
98 def emitrevisiondeltas(self, requests):
99 return self._revlog.emitrevisiondeltas(requests)
99 return self._revlog.emitrevisiondeltas(requests)
100
100
101 def addrevision(self, revisiondata, transaction, linkrev, p1, p2,
101 def addrevision(self, revisiondata, transaction, linkrev, p1, p2,
102 node=None, flags=revlog.REVIDX_DEFAULT_FLAGS,
102 node=None, flags=revlog.REVIDX_DEFAULT_FLAGS,
103 cachedelta=None):
103 cachedelta=None):
104 return self._revlog.addrevision(revisiondata, transaction, linkrev,
104 return self._revlog.addrevision(revisiondata, transaction, linkrev,
105 p1, p2, node=node, flags=flags,
105 p1, p2, node=node, flags=flags,
106 cachedelta=cachedelta)
106 cachedelta=cachedelta)
107
107
108 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
108 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
109 return self._revlog.addgroup(deltas, linkmapper, transaction,
109 return self._revlog.addgroup(deltas, linkmapper, transaction,
110 addrevisioncb=addrevisioncb)
110 addrevisioncb=addrevisioncb)
111
111
112 def getstrippoint(self, minlink):
112 def getstrippoint(self, minlink):
113 return self._revlog.getstrippoint(minlink)
113 return self._revlog.getstrippoint(minlink)
114
114
115 def strip(self, minlink, transaction):
115 def strip(self, minlink, transaction):
116 return self._revlog.strip(minlink, transaction)
116 return self._revlog.strip(minlink, transaction)
117
117
118 def censorrevision(self, tr, node, tombstone=b''):
118 def censorrevision(self, tr, node, tombstone=b''):
119 return self._revlog.censorrevision(node, tombstone=tombstone)
119 return self._revlog.censorrevision(node, tombstone=tombstone)
120
120
121 def files(self):
121 def files(self):
122 return self._revlog.files()
122 return self._revlog.files()
123
123
124 def read(self, node):
124 def read(self, node):
125 t = self.revision(node)
125 t = self.revision(node)
126 if not t.startswith('\1\n'):
126 if not t.startswith('\1\n'):
127 return t
127 return t
128 s = t.index('\1\n', 2)
128 s = t.index('\1\n', 2)
129 return t[s + 2:]
129 return t[s + 2:]
130
130
131 def add(self, text, meta, transaction, link, p1=None, p2=None):
131 def add(self, text, meta, transaction, link, p1=None, p2=None):
132 if meta or text.startswith('\1\n'):
132 if meta or text.startswith('\1\n'):
133 text = revlog.packmeta(meta, text)
133 text = revlog.packmeta(meta, text)
134 return self.addrevision(text, transaction, link, p1, p2)
134 return self.addrevision(text, transaction, link, p1, p2)
135
135
136 def renamed(self, node):
136 def renamed(self, node):
137 if self.parents(node)[0] != revlog.nullid:
137 if self.parents(node)[0] != revlog.nullid:
138 return False
138 return False
139 t = self.revision(node)
139 t = self.revision(node)
140 m = revlog.parsemeta(t)[0]
140 m = revlog.parsemeta(t)[0]
141 # copy and copyrev occur in pairs. In rare cases due to bugs,
141 # copy and copyrev occur in pairs. In rare cases due to bugs,
142 # one can occur without the other.
142 # one can occur without the other.
143 if m and "copy" in m and "copyrev" in m:
143 if m and "copy" in m and "copyrev" in m:
144 return (m["copy"], revlog.bin(m["copyrev"]))
144 return (m["copy"], revlog.bin(m["copyrev"]))
145 return False
145 return False
146
146
147 def size(self, rev):
147 def size(self, rev):
148 """return the size of a given revision"""
148 """return the size of a given revision"""
149
149
150 # for revisions with renames, we have to go the slow way
150 # for revisions with renames, we have to go the slow way
151 node = self.node(rev)
151 node = self.node(rev)
152 if self.renamed(node):
152 if self.renamed(node):
153 return len(self.read(node))
153 return len(self.read(node))
154 if self.iscensored(rev):
154 if self.iscensored(rev):
155 return 0
155 return 0
156
156
157 # XXX if self.read(node).startswith("\1\n"), this returns (size+4)
157 # XXX if self.read(node).startswith("\1\n"), this returns (size+4)
158 return self._revlog.size(rev)
158 return self._revlog.size(rev)
159
159
160 def cmp(self, node, text):
160 def cmp(self, node, text):
161 """compare text with a given file revision
161 """compare text with a given file revision
162
162
163 returns True if text is different than what is stored.
163 returns True if text is different than what is stored.
164 """
164 """
165
165
166 t = text
166 t = text
167 if text.startswith('\1\n'):
167 if text.startswith('\1\n'):
168 t = '\1\n\1\n' + text
168 t = '\1\n\1\n' + text
169
169
170 samehashes = not self._revlog.cmp(node, t)
170 samehashes = not self._revlog.cmp(node, t)
171 if samehashes:
171 if samehashes:
172 return False
172 return False
173
173
174 # censored files compare against the empty file
174 # censored files compare against the empty file
175 if self.iscensored(self.rev(node)):
175 if self.iscensored(self.rev(node)):
176 return text != ''
176 return text != ''
177
177
178 # renaming a file produces a different hash, even if the data
178 # renaming a file produces a different hash, even if the data
179 # remains unchanged. Check if it's the case (slow):
179 # remains unchanged. Check if it's the case (slow):
180 if self.renamed(node):
180 if self.renamed(node):
181 t2 = self.read(node)
181 t2 = self.read(node)
182 return t2 != text
182 return t2 != text
183
183
184 return True
184 return True
185
185
186 def verifyintegrity(self, state):
186 def verifyintegrity(self, state):
187 return self._revlog.verifyintegrity(state)
187 return self._revlog.verifyintegrity(state)
188
188
189 # TODO these aren't part of the interface and aren't internal methods.
189 # TODO these aren't part of the interface and aren't internal methods.
190 # Callers should be fixed to not use them.
190 # Callers should be fixed to not use them.
191
191
192 # Used by LFS.
193 @property
194 def filename(self):
195 return self._revlog.filename
196
197 @filename.setter
198 def filename(self, value):
199 self._revlog.filename = value
200
201 # Used by bundlefilelog, unionfilelog.
192 # Used by bundlefilelog, unionfilelog.
202 @property
193 @property
203 def indexfile(self):
194 def indexfile(self):
204 return self._revlog.indexfile
195 return self._revlog.indexfile
205
196
206 @indexfile.setter
197 @indexfile.setter
207 def indexfile(self, value):
198 def indexfile(self, value):
208 self._revlog.indexfile = value
199 self._revlog.indexfile = value
209
200
210 # Used by repo upgrade.
201 # Used by repo upgrade.
211 @property
202 @property
212 def opener(self):
203 def opener(self):
213 return self._revlog.opener
204 return self._revlog.opener
214
205
215 # Used by repo upgrade.
206 # Used by repo upgrade.
216 def clone(self, tr, destrevlog, **kwargs):
207 def clone(self, tr, destrevlog, **kwargs):
217 if not isinstance(destrevlog, filelog):
208 if not isinstance(destrevlog, filelog):
218 raise error.ProgrammingError('expected filelog to clone()')
209 raise error.ProgrammingError('expected filelog to clone()')
219
210
220 return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
211 return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
221
212
222 class narrowfilelog(filelog):
213 class narrowfilelog(filelog):
223 """Filelog variation to be used with narrow stores."""
214 """Filelog variation to be used with narrow stores."""
224
215
225 def __init__(self, opener, path, narrowmatch):
216 def __init__(self, opener, path, narrowmatch):
226 super(narrowfilelog, self).__init__(opener, path)
217 super(narrowfilelog, self).__init__(opener, path)
227 self._narrowmatch = narrowmatch
218 self._narrowmatch = narrowmatch
228
219
229 def renamed(self, node):
220 def renamed(self, node):
230 res = super(narrowfilelog, self).renamed(node)
221 res = super(narrowfilelog, self).renamed(node)
231
222
232 # Renames that come from outside the narrowspec are problematic
223 # Renames that come from outside the narrowspec are problematic
233 # because we may lack the base text for the rename. This can result
224 # because we may lack the base text for the rename. This can result
234 # in code attempting to walk the ancestry or compute a diff
225 # in code attempting to walk the ancestry or compute a diff
235 # encountering a missing revision. We address this by silently
226 # encountering a missing revision. We address this by silently
236 # removing rename metadata if the source file is outside the
227 # removing rename metadata if the source file is outside the
237 # narrow spec.
228 # narrow spec.
238 #
229 #
239 # A better solution would be to see if the base revision is available,
230 # A better solution would be to see if the base revision is available,
240 # rather than assuming it isn't.
231 # rather than assuming it isn't.
241 #
232 #
242 # An even better solution would be to teach all consumers of rename
233 # An even better solution would be to teach all consumers of rename
243 # metadata that the base revision may not be available.
234 # metadata that the base revision may not be available.
244 #
235 #
245 # TODO consider better ways of doing this.
236 # TODO consider better ways of doing this.
246 if res and not self._narrowmatch(res[0]):
237 if res and not self._narrowmatch(res[0]):
247 return None
238 return None
248
239
249 return res
240 return res
250
241
251 def size(self, rev):
242 def size(self, rev):
252 # Because we have a custom renamed() that may lie, we need to call
243 # Because we have a custom renamed() that may lie, we need to call
253 # the base renamed() to report accurate results.
244 # the base renamed() to report accurate results.
254 node = self.node(rev)
245 node = self.node(rev)
255 if super(narrowfilelog, self).renamed(node):
246 if super(narrowfilelog, self).renamed(node):
256 return len(self.read(node))
247 return len(self.read(node))
257 else:
248 else:
258 return super(narrowfilelog, self).size(rev)
249 return super(narrowfilelog, self).size(rev)
259
250
260 def cmp(self, node, text):
251 def cmp(self, node, text):
261 different = super(narrowfilelog, self).cmp(node, text)
252 different = super(narrowfilelog, self).cmp(node, text)
262
253
263 # Because renamed() may lie, we may get false positives for
254 # Because renamed() may lie, we may get false positives for
264 # different content. Check for this by comparing against the original
255 # different content. Check for this by comparing against the original
265 # renamed() implementation.
256 # renamed() implementation.
266 if different:
257 if different:
267 if super(narrowfilelog, self).renamed(node):
258 if super(narrowfilelog, self).renamed(node):
268 t2 = self.read(node)
259 t2 = self.read(node)
269 return t2 != text
260 return t2 != text
270
261
271 return different
262 return different
General Comments 0
You need to be logged in to leave comments. Login now