##// END OF EJS Templates
lfs: allow to run 'debugupgraderepo' on repo with largefiles...
Boris Feld -
r35347:9eb19b13 default
parent child Browse files
Show More
@@ -1,191 +1,198
1 1 # lfs - hash-preserving large file support using Git-LFS protocol
2 2 #
3 3 # Copyright 2017 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """lfs - large file support (EXPERIMENTAL)
9 9
10 10 Configs::
11 11
12 12 [lfs]
13 13 # Remote endpoint. Multiple protocols are supported:
14 14 # - http(s)://user:pass@example.com/path
15 15 # git-lfs endpoint
16 16 # - file:///tmp/path
17 17 # local filesystem, usually for testing
18 18 # if unset, lfs will prompt setting this when it must use this value.
19 19 # (default: unset)
20 20 url = https://example.com/lfs
21 21
22 22 # size of a file to make it use LFS
23 23 threshold = 10M
24 24
25 25 # how many times to retry before giving up on transferring an object
26 26 retry = 5
27 27
28 28 # the local directory to store lfs files for sharing across local clones.
29 29 # If not set, the cache is located in an OS specific cache location.
30 30 usercache = /path/to/global/cache
31 31 """
32 32
33 33 from __future__ import absolute_import
34 34
35 35 from mercurial.i18n import _
36 36
37 37 from mercurial import (
38 38 bundle2,
39 39 changegroup,
40 40 context,
41 41 exchange,
42 42 extensions,
43 43 filelog,
44 44 hg,
45 45 localrepo,
46 46 registrar,
47 47 revlog,
48 48 scmutil,
49 upgrade,
49 50 vfs as vfsmod,
50 51 )
51 52
52 53 from . import (
53 54 blobstore,
54 55 wrapper,
55 56 )
56 57
57 58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
58 59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
59 60 # be specifying the version(s) of Mercurial they are tested with, or
60 61 # leave the attribute unspecified.
61 62 testedwith = 'ships-with-hg-core'
62 63
63 64 configtable = {}
64 65 configitem = registrar.configitem(configtable)
65 66
66 67 configitem('lfs', 'url',
67 68 default=configitem.dynamicdefault,
68 69 )
69 70 configitem('lfs', 'usercache',
70 71 default=None,
71 72 )
72 73 configitem('lfs', 'threshold',
73 74 default=None,
74 75 )
75 76 configitem('lfs', 'retry',
76 77 default=5,
77 78 )
78 79 # Deprecated
79 80 configitem('lfs', 'remotestore',
80 81 default=None,
81 82 )
82 83 # Deprecated
83 84 configitem('lfs', 'dummy',
84 85 default=None,
85 86 )
86 87 # Deprecated
87 88 configitem('lfs', 'git-lfs',
88 89 default=None,
89 90 )
90 91
91 92 cmdtable = {}
92 93 command = registrar.command(cmdtable)
93 94
94 95 templatekeyword = registrar.templatekeyword()
95 96
96 97 def featuresetup(ui, supported):
97 98 # don't die on seeing a repo with the lfs requirement
98 99 supported |= {'lfs'}
99 100
100 101 def uisetup(ui):
101 102 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
102 103
103 104 def reposetup(ui, repo):
104 105 # Nothing to do with a remote repo
105 106 if not repo.local():
106 107 return
107 108
108 109 threshold = repo.ui.configbytes('lfs', 'threshold')
109 110
110 111 repo.svfs.options['lfsthreshold'] = threshold
111 112 repo.svfs.lfslocalblobstore = blobstore.local(repo)
112 113 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
113 114
114 115 # Push hook
115 116 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
116 117
117 118 if 'lfs' not in repo.requirements:
118 119 def checkrequireslfs(ui, repo, **kwargs):
119 120 if 'lfs' not in repo.requirements:
120 121 ctx = repo[kwargs['node']]
121 122 # TODO: is there a way to just walk the files in the commit?
122 123 if any(ctx[f].islfs() for f in ctx.files()):
123 124 repo.requirements.add('lfs')
124 125 repo._writerequirements()
125 126
126 127 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
127 128
128 129 def wrapfilelog(filelog):
129 130 wrapfunction = extensions.wrapfunction
130 131
131 132 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
132 133 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
133 134 wrapfunction(filelog, 'size', wrapper.filelogsize)
134 135
135 136 def extsetup(ui):
136 137 wrapfilelog(filelog.filelog)
137 138
138 139 wrapfunction = extensions.wrapfunction
139 140
140 141 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
141 142
143 wrapfunction(upgrade, 'preservedrequirements',
144 wrapper.upgraderequirements)
145
146 wrapfunction(upgrade, 'supporteddestrequirements',
147 wrapper.upgraderequirements)
148
142 149 wrapfunction(changegroup,
143 150 'supportedoutgoingversions',
144 151 wrapper.supportedoutgoingversions)
145 152 wrapfunction(changegroup,
146 153 'allsupportedversions',
147 154 wrapper.allsupportedversions)
148 155
149 156 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
150 157 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
151 158 context.basefilectx.islfs = wrapper.filectxislfs
152 159
153 160 revlog.addflagprocessor(
154 161 revlog.REVIDX_EXTSTORED,
155 162 (
156 163 wrapper.readfromstore,
157 164 wrapper.writetostore,
158 165 wrapper.bypasscheckhash,
159 166 ),
160 167 )
161 168
162 169 wrapfunction(hg, 'clone', wrapper.hgclone)
163 170 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
164 171
165 172 # Make bundle choose changegroup3 instead of changegroup2. This affects
166 173 # "hg bundle" command. Note: it does not cover all bundle formats like
167 174 # "packed1". Using "packed1" with lfs will likely cause trouble.
168 175 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
169 176 for k in names:
170 177 exchange._bundlespeccgversions[k] = '03'
171 178
172 179 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
173 180 # options and blob stores are passed from othervfs to the new readonlyvfs.
174 181 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
175 182
176 183 # when writing a bundle via "hg bundle" command, upload related LFS blobs
177 184 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
178 185
179 186 @templatekeyword('lfs_files')
180 187 def lfsfiles(repo, ctx, **args):
181 188 """List of strings. LFS files added or modified by the changeset."""
182 189 pointers = wrapper.pointersfromctx(ctx) # {path: pointer}
183 190 return sorted(pointers.keys())
184 191
185 192 @command('debuglfsupload',
186 193 [('r', 'rev', [], _('upload large files introduced by REV'))])
187 194 def debuglfsupload(ui, repo, **opts):
188 195 """upload lfs blobs added by the working copy parent or given revisions"""
189 196 revs = opts.get('rev', [])
190 197 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
191 198 wrapper.uploadblobs(repo, pointers)
@@ -1,304 +1,310
1 1 # wrapper.py - methods wrapping core mercurial logic
2 2 #
3 3 # Copyright 2017 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import hashlib
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial.node import bin, nullid, short
14 14
15 15 from mercurial import (
16 16 error,
17 17 filelog,
18 18 revlog,
19 19 util,
20 20 )
21 21
22 22 from . import (
23 23 blobstore,
24 24 pointer,
25 25 )
26 26
27 27 def supportedoutgoingversions(orig, repo):
28 28 versions = orig(repo)
29 29 versions.discard('01')
30 30 versions.discard('02')
31 31 versions.add('03')
32 32 return versions
33 33
34 34 def allsupportedversions(orig, ui):
35 35 versions = orig(ui)
36 36 versions.add('03')
37 37 return versions
38 38
39 39 def bypasscheckhash(self, text):
40 40 return False
41 41
42 42 def readfromstore(self, text):
43 43 """Read filelog content from local blobstore transform for flagprocessor.
44 44
45 45 Default tranform for flagprocessor, returning contents from blobstore.
46 46 Returns a 2-typle (text, validatehash) where validatehash is True as the
47 47 contents of the blobstore should be checked using checkhash.
48 48 """
49 49 p = pointer.deserialize(text)
50 50 oid = p.oid()
51 51 store = self.opener.lfslocalblobstore
52 52 if not store.has(oid):
53 53 p.filename = getattr(self, 'indexfile', None)
54 54 self.opener.lfsremoteblobstore.readbatch([p], store)
55 55 text = store.read(oid)
56 56
57 57 # pack hg filelog metadata
58 58 hgmeta = {}
59 59 for k in p.keys():
60 60 if k.startswith('x-hg-'):
61 61 name = k[len('x-hg-'):]
62 62 hgmeta[name] = p[k]
63 63 if hgmeta or text.startswith('\1\n'):
64 64 text = filelog.packmeta(hgmeta, text)
65 65
66 66 return (text, True)
67 67
68 68 def writetostore(self, text):
69 69 # hg filelog metadata (includes rename, etc)
70 70 hgmeta, offset = filelog.parsemeta(text)
71 71 if offset and offset > 0:
72 72 # lfs blob does not contain hg filelog metadata
73 73 text = text[offset:]
74 74
75 75 # git-lfs only supports sha256
76 76 oid = hashlib.sha256(text).hexdigest()
77 77 self.opener.lfslocalblobstore.write(oid, text)
78 78
79 79 # replace contents with metadata
80 80 longoid = 'sha256:%s' % oid
81 81 metadata = pointer.gitlfspointer(oid=longoid, size=str(len(text)))
82 82
83 83 # by default, we expect the content to be binary. however, LFS could also
84 84 # be used for non-binary content. add a special entry for non-binary data.
85 85 # this will be used by filectx.isbinary().
86 86 if not util.binary(text):
87 87 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
88 88 metadata['x-is-binary'] = '0'
89 89
90 90 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
91 91 if hgmeta is not None:
92 92 for k, v in hgmeta.iteritems():
93 93 metadata['x-hg-%s' % k] = v
94 94
95 95 rawtext = metadata.serialize()
96 96 return (rawtext, False)
97 97
98 98 def _islfs(rlog, node=None, rev=None):
99 99 if rev is None:
100 100 if node is None:
101 101 # both None - likely working copy content where node is not ready
102 102 return False
103 103 rev = rlog.rev(node)
104 104 else:
105 105 node = rlog.node(rev)
106 106 if node == nullid:
107 107 return False
108 108 flags = rlog.flags(rev)
109 109 return bool(flags & revlog.REVIDX_EXTSTORED)
110 110
111 111 def filelogaddrevision(orig, self, text, transaction, link, p1, p2,
112 112 cachedelta=None, node=None,
113 113 flags=revlog.REVIDX_DEFAULT_FLAGS, **kwds):
114 114 threshold = self.opener.options['lfsthreshold']
115 115 textlen = len(text)
116 116 # exclude hg rename meta from file size
117 117 meta, offset = filelog.parsemeta(text)
118 118 if offset:
119 119 textlen -= offset
120 120
121 121 if threshold and textlen > threshold:
122 122 flags |= revlog.REVIDX_EXTSTORED
123 123
124 124 return orig(self, text, transaction, link, p1, p2, cachedelta=cachedelta,
125 125 node=node, flags=flags, **kwds)
126 126
127 127 def filelogrenamed(orig, self, node):
128 128 if _islfs(self, node):
129 129 rawtext = self.revision(node, raw=True)
130 130 if not rawtext:
131 131 return False
132 132 metadata = pointer.deserialize(rawtext)
133 133 if 'x-hg-copy' in metadata and 'x-hg-copyrev' in metadata:
134 134 return metadata['x-hg-copy'], bin(metadata['x-hg-copyrev'])
135 135 else:
136 136 return False
137 137 return orig(self, node)
138 138
139 139 def filelogsize(orig, self, rev):
140 140 if _islfs(self, rev=rev):
141 141 # fast path: use lfs metadata to answer size
142 142 rawtext = self.revision(rev, raw=True)
143 143 metadata = pointer.deserialize(rawtext)
144 144 return int(metadata['size'])
145 145 return orig(self, rev)
146 146
147 147 def filectxcmp(orig, self, fctx):
148 148 """returns True if text is different than fctx"""
149 149 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
150 150 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
151 151 # fast path: check LFS oid
152 152 p1 = pointer.deserialize(self.rawdata())
153 153 p2 = pointer.deserialize(fctx.rawdata())
154 154 return p1.oid() != p2.oid()
155 155 return orig(self, fctx)
156 156
157 157 def filectxisbinary(orig, self):
158 158 if self.islfs():
159 159 # fast path: use lfs metadata to answer isbinary
160 160 metadata = pointer.deserialize(self.rawdata())
161 161 # if lfs metadata says nothing, assume it's binary by default
162 162 return bool(int(metadata.get('x-is-binary', 1)))
163 163 return orig(self)
164 164
165 165 def filectxislfs(self):
166 166 return _islfs(self.filelog(), self.filenode())
167 167
168 168 def convertsink(orig, sink):
169 169 sink = orig(sink)
170 170 if sink.repotype == 'hg':
171 171 class lfssink(sink.__class__):
172 172 def putcommit(self, files, copies, parents, commit, source, revmap,
173 173 full, cleanp2):
174 174 pc = super(lfssink, self).putcommit
175 175 node = pc(files, copies, parents, commit, source, revmap, full,
176 176 cleanp2)
177 177
178 178 if 'lfs' not in self.repo.requirements:
179 179 ctx = self.repo[node]
180 180
181 181 # The file list may contain removed files, so check for
182 182 # membership before assuming it is in the context.
183 183 if any(f in ctx and ctx[f].islfs() for f, n in files):
184 184 self.repo.requirements.add('lfs')
185 185 self.repo._writerequirements()
186 186
187 187 # Permanently enable lfs locally
188 188 with self.repo.vfs('hgrc', 'a', text=True) as fp:
189 189 fp.write('\n[extensions]\nlfs=\n')
190 190
191 191 return node
192 192
193 193 sink.__class__ = lfssink
194 194
195 195 return sink
196 196
197 197 def vfsinit(orig, self, othervfs):
198 198 orig(self, othervfs)
199 199 # copy lfs related options
200 200 for k, v in othervfs.options.items():
201 201 if k.startswith('lfs'):
202 202 self.options[k] = v
203 203 # also copy lfs blobstores. note: this can run before reposetup, so lfs
204 204 # blobstore attributes are not always ready at this time.
205 205 for name in ['lfslocalblobstore', 'lfsremoteblobstore']:
206 206 if util.safehasattr(othervfs, name):
207 207 setattr(self, name, getattr(othervfs, name))
208 208
209 209 def hgclone(orig, ui, opts, *args, **kwargs):
210 210 result = orig(ui, opts, *args, **kwargs)
211 211
212 212 if result is not None:
213 213 sourcerepo, destrepo = result
214 214 repo = destrepo.local()
215 215
216 216 # When cloning to a remote repo (like through SSH), no repo is available
217 217 # from the peer. Therefore the hgrc can't be updated.
218 218 if not repo:
219 219 return result
220 220
221 221 # If lfs is required for this repo, permanently enable it locally
222 222 if 'lfs' in repo.requirements:
223 223 with repo.vfs('hgrc', 'a', text=True) as fp:
224 224 fp.write('\n[extensions]\nlfs=\n')
225 225
226 226 return result
227 227
228 228 def hgpostshare(orig, sourcerepo, destrepo, bookmarks=True, defaultpath=None):
229 229 orig(sourcerepo, destrepo, bookmarks, defaultpath)
230 230
231 231 # If lfs is required for this repo, permanently enable it locally
232 232 if 'lfs' in destrepo.requirements:
233 233 with destrepo.vfs('hgrc', 'a', text=True) as fp:
234 234 fp.write('\n[extensions]\nlfs=\n')
235 235
236 236 def _canskipupload(repo):
237 237 # if remotestore is a null store, upload is a no-op and can be skipped
238 238 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
239 239
240 240 def candownload(repo):
241 241 # if remotestore is a null store, downloads will lead to nothing
242 242 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
243 243
244 244 def uploadblobsfromrevs(repo, revs):
245 245 '''upload lfs blobs introduced by revs
246 246
247 247 Note: also used by other extensions e. g. infinitepush. avoid renaming.
248 248 '''
249 249 if _canskipupload(repo):
250 250 return
251 251 pointers = extractpointers(repo, revs)
252 252 uploadblobs(repo, pointers)
253 253
254 254 def prepush(pushop):
255 255 """Prepush hook.
256 256
257 257 Read through the revisions to push, looking for filelog entries that can be
258 258 deserialized into metadata so that we can block the push on their upload to
259 259 the remote blobstore.
260 260 """
261 261 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
262 262
263 263 def writenewbundle(orig, ui, repo, source, filename, bundletype, outgoing,
264 264 *args, **kwargs):
265 265 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
266 266 uploadblobsfromrevs(repo, outgoing.missing)
267 267 return orig(ui, repo, source, filename, bundletype, outgoing, *args,
268 268 **kwargs)
269 269
270 270 def extractpointers(repo, revs):
271 271 """return a list of lfs pointers added by given revs"""
272 272 ui = repo.ui
273 273 if ui.debugflag:
274 274 ui.write(_('lfs: computing set of blobs to upload\n'))
275 275 pointers = {}
276 276 for r in revs:
277 277 ctx = repo[r]
278 278 for p in pointersfromctx(ctx).values():
279 279 pointers[p.oid()] = p
280 280 return pointers.values()
281 281
282 282 def pointersfromctx(ctx):
283 283 """return a dict {path: pointer} for given single changectx"""
284 284 result = {}
285 285 for f in ctx.files():
286 286 if f not in ctx:
287 287 continue
288 288 fctx = ctx[f]
289 289 if not _islfs(fctx.filelog(), fctx.filenode()):
290 290 continue
291 291 try:
292 292 result[f] = pointer.deserialize(fctx.rawdata())
293 293 except pointer.InvalidPointer as ex:
294 294 raise error.Abort(_('lfs: corrupted pointer (%s@%s): %s\n')
295 295 % (f, short(ctx.node()), ex))
296 296 return result
297 297
298 298 def uploadblobs(repo, pointers):
299 299 """upload given pointers from local blobstore"""
300 300 if not pointers:
301 301 return
302 302
303 303 remoteblob = repo.svfs.lfsremoteblobstore
304 304 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
305
306 def upgraderequirements(orig, repo):
307 reqs = orig(repo)
308 if 'lfs' in repo.requirements:
309 reqs.add('lfs')
310 return reqs
General Comments 0
You need to be logged in to leave comments. Login now