##// END OF EJS Templates
engine: make hook point for extension a public function...
Pulkit Goyal -
r46836:481d9aed default
parent child Browse files
Show More
@@ -1,550 +1,550 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 from mercurial.pycompat import (
14 from mercurial.pycompat import (
15 getattr,
15 getattr,
16 setattr,
16 setattr,
17 )
17 )
18
18
19 from mercurial import (
19 from mercurial import (
20 bundle2,
20 bundle2,
21 changegroup,
21 changegroup,
22 cmdutil,
22 cmdutil,
23 context,
23 context,
24 error,
24 error,
25 exchange,
25 exchange,
26 exthelper,
26 exthelper,
27 localrepo,
27 localrepo,
28 pycompat,
28 pycompat,
29 revlog,
29 revlog,
30 scmutil,
30 scmutil,
31 util,
31 util,
32 vfs as vfsmod,
32 vfs as vfsmod,
33 wireprotov1server,
33 wireprotov1server,
34 )
34 )
35
35
36 from mercurial.upgrade_utils import (
36 from mercurial.upgrade_utils import (
37 actions as upgrade_actions,
37 actions as upgrade_actions,
38 engine as upgrade_engine,
38 engine as upgrade_engine,
39 )
39 )
40
40
41 from mercurial.interfaces import repository
41 from mercurial.interfaces import repository
42
42
43 from mercurial.utils import (
43 from mercurial.utils import (
44 storageutil,
44 storageutil,
45 stringutil,
45 stringutil,
46 )
46 )
47
47
48 from ..largefiles import lfutil
48 from ..largefiles import lfutil
49
49
50 from . import (
50 from . import (
51 blobstore,
51 blobstore,
52 pointer,
52 pointer,
53 )
53 )
54
54
55 eh = exthelper.exthelper()
55 eh = exthelper.exthelper()
56
56
57
57
58 @eh.wrapfunction(localrepo, b'makefilestorage')
58 @eh.wrapfunction(localrepo, b'makefilestorage')
59 def localrepomakefilestorage(orig, requirements, features, **kwargs):
59 def localrepomakefilestorage(orig, requirements, features, **kwargs):
60 if b'lfs' in requirements:
60 if b'lfs' in requirements:
61 features.add(repository.REPO_FEATURE_LFS)
61 features.add(repository.REPO_FEATURE_LFS)
62
62
63 return orig(requirements=requirements, features=features, **kwargs)
63 return orig(requirements=requirements, features=features, **kwargs)
64
64
65
65
66 @eh.wrapfunction(changegroup, b'allsupportedversions')
66 @eh.wrapfunction(changegroup, b'allsupportedversions')
67 def allsupportedversions(orig, ui):
67 def allsupportedversions(orig, ui):
68 versions = orig(ui)
68 versions = orig(ui)
69 versions.add(b'03')
69 versions.add(b'03')
70 return versions
70 return versions
71
71
72
72
73 @eh.wrapfunction(wireprotov1server, b'_capabilities')
73 @eh.wrapfunction(wireprotov1server, b'_capabilities')
74 def _capabilities(orig, repo, proto):
74 def _capabilities(orig, repo, proto):
75 '''Wrap server command to announce lfs server capability'''
75 '''Wrap server command to announce lfs server capability'''
76 caps = orig(repo, proto)
76 caps = orig(repo, proto)
77 if util.safehasattr(repo.svfs, b'lfslocalblobstore'):
77 if util.safehasattr(repo.svfs, b'lfslocalblobstore'):
78 # Advertise a slightly different capability when lfs is *required*, so
78 # Advertise a slightly different capability when lfs is *required*, so
79 # that the client knows it MUST load the extension. If lfs is not
79 # that the client knows it MUST load the extension. If lfs is not
80 # required on the server, there's no reason to autoload the extension
80 # required on the server, there's no reason to autoload the extension
81 # on the client.
81 # on the client.
82 if b'lfs' in repo.requirements:
82 if b'lfs' in repo.requirements:
83 caps.append(b'lfs-serve')
83 caps.append(b'lfs-serve')
84
84
85 caps.append(b'lfs')
85 caps.append(b'lfs')
86 return caps
86 return caps
87
87
88
88
89 def bypasscheckhash(self, text):
89 def bypasscheckhash(self, text):
90 return False
90 return False
91
91
92
92
93 def readfromstore(self, text):
93 def readfromstore(self, text):
94 """Read filelog content from local blobstore transform for flagprocessor.
94 """Read filelog content from local blobstore transform for flagprocessor.
95
95
96 Default tranform for flagprocessor, returning contents from blobstore.
96 Default tranform for flagprocessor, returning contents from blobstore.
97 Returns a 2-typle (text, validatehash) where validatehash is True as the
97 Returns a 2-typle (text, validatehash) where validatehash is True as the
98 contents of the blobstore should be checked using checkhash.
98 contents of the blobstore should be checked using checkhash.
99 """
99 """
100 p = pointer.deserialize(text)
100 p = pointer.deserialize(text)
101 oid = p.oid()
101 oid = p.oid()
102 store = self.opener.lfslocalblobstore
102 store = self.opener.lfslocalblobstore
103 if not store.has(oid):
103 if not store.has(oid):
104 p.filename = self.filename
104 p.filename = self.filename
105 self.opener.lfsremoteblobstore.readbatch([p], store)
105 self.opener.lfsremoteblobstore.readbatch([p], store)
106
106
107 # The caller will validate the content
107 # The caller will validate the content
108 text = store.read(oid, verify=False)
108 text = store.read(oid, verify=False)
109
109
110 # pack hg filelog metadata
110 # pack hg filelog metadata
111 hgmeta = {}
111 hgmeta = {}
112 for k in p.keys():
112 for k in p.keys():
113 if k.startswith(b'x-hg-'):
113 if k.startswith(b'x-hg-'):
114 name = k[len(b'x-hg-') :]
114 name = k[len(b'x-hg-') :]
115 hgmeta[name] = p[k]
115 hgmeta[name] = p[k]
116 if hgmeta or text.startswith(b'\1\n'):
116 if hgmeta or text.startswith(b'\1\n'):
117 text = storageutil.packmeta(hgmeta, text)
117 text = storageutil.packmeta(hgmeta, text)
118
118
119 return (text, True, {})
119 return (text, True, {})
120
120
121
121
122 def writetostore(self, text, sidedata):
122 def writetostore(self, text, sidedata):
123 # hg filelog metadata (includes rename, etc)
123 # hg filelog metadata (includes rename, etc)
124 hgmeta, offset = storageutil.parsemeta(text)
124 hgmeta, offset = storageutil.parsemeta(text)
125 if offset and offset > 0:
125 if offset and offset > 0:
126 # lfs blob does not contain hg filelog metadata
126 # lfs blob does not contain hg filelog metadata
127 text = text[offset:]
127 text = text[offset:]
128
128
129 # git-lfs only supports sha256
129 # git-lfs only supports sha256
130 oid = hex(hashlib.sha256(text).digest())
130 oid = hex(hashlib.sha256(text).digest())
131 self.opener.lfslocalblobstore.write(oid, text)
131 self.opener.lfslocalblobstore.write(oid, text)
132
132
133 # replace contents with metadata
133 # replace contents with metadata
134 longoid = b'sha256:%s' % oid
134 longoid = b'sha256:%s' % oid
135 metadata = pointer.gitlfspointer(oid=longoid, size=b'%d' % len(text))
135 metadata = pointer.gitlfspointer(oid=longoid, size=b'%d' % len(text))
136
136
137 # by default, we expect the content to be binary. however, LFS could also
137 # by default, we expect the content to be binary. however, LFS could also
138 # be used for non-binary content. add a special entry for non-binary data.
138 # be used for non-binary content. add a special entry for non-binary data.
139 # this will be used by filectx.isbinary().
139 # this will be used by filectx.isbinary().
140 if not stringutil.binary(text):
140 if not stringutil.binary(text):
141 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
141 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
142 metadata[b'x-is-binary'] = b'0'
142 metadata[b'x-is-binary'] = b'0'
143
143
144 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
144 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
145 if hgmeta is not None:
145 if hgmeta is not None:
146 for k, v in pycompat.iteritems(hgmeta):
146 for k, v in pycompat.iteritems(hgmeta):
147 metadata[b'x-hg-%s' % k] = v
147 metadata[b'x-hg-%s' % k] = v
148
148
149 rawtext = metadata.serialize()
149 rawtext = metadata.serialize()
150 return (rawtext, False)
150 return (rawtext, False)
151
151
152
152
153 def _islfs(rlog, node=None, rev=None):
153 def _islfs(rlog, node=None, rev=None):
154 if rev is None:
154 if rev is None:
155 if node is None:
155 if node is None:
156 # both None - likely working copy content where node is not ready
156 # both None - likely working copy content where node is not ready
157 return False
157 return False
158 rev = rlog.rev(node)
158 rev = rlog.rev(node)
159 else:
159 else:
160 node = rlog.node(rev)
160 node = rlog.node(rev)
161 if node == nullid:
161 if node == nullid:
162 return False
162 return False
163 flags = rlog.flags(rev)
163 flags = rlog.flags(rev)
164 return bool(flags & revlog.REVIDX_EXTSTORED)
164 return bool(flags & revlog.REVIDX_EXTSTORED)
165
165
166
166
167 # Wrapping may also be applied by remotefilelog
167 # Wrapping may also be applied by remotefilelog
168 def filelogaddrevision(
168 def filelogaddrevision(
169 orig,
169 orig,
170 self,
170 self,
171 text,
171 text,
172 transaction,
172 transaction,
173 link,
173 link,
174 p1,
174 p1,
175 p2,
175 p2,
176 cachedelta=None,
176 cachedelta=None,
177 node=None,
177 node=None,
178 flags=revlog.REVIDX_DEFAULT_FLAGS,
178 flags=revlog.REVIDX_DEFAULT_FLAGS,
179 **kwds
179 **kwds
180 ):
180 ):
181 # The matcher isn't available if reposetup() wasn't called.
181 # The matcher isn't available if reposetup() wasn't called.
182 lfstrack = self._revlog.opener.options.get(b'lfstrack')
182 lfstrack = self._revlog.opener.options.get(b'lfstrack')
183
183
184 if lfstrack:
184 if lfstrack:
185 textlen = len(text)
185 textlen = len(text)
186 # exclude hg rename meta from file size
186 # exclude hg rename meta from file size
187 meta, offset = storageutil.parsemeta(text)
187 meta, offset = storageutil.parsemeta(text)
188 if offset:
188 if offset:
189 textlen -= offset
189 textlen -= offset
190
190
191 if lfstrack(self._revlog.filename, textlen):
191 if lfstrack(self._revlog.filename, textlen):
192 flags |= revlog.REVIDX_EXTSTORED
192 flags |= revlog.REVIDX_EXTSTORED
193
193
194 return orig(
194 return orig(
195 self,
195 self,
196 text,
196 text,
197 transaction,
197 transaction,
198 link,
198 link,
199 p1,
199 p1,
200 p2,
200 p2,
201 cachedelta=cachedelta,
201 cachedelta=cachedelta,
202 node=node,
202 node=node,
203 flags=flags,
203 flags=flags,
204 **kwds
204 **kwds
205 )
205 )
206
206
207
207
208 # Wrapping may also be applied by remotefilelog
208 # Wrapping may also be applied by remotefilelog
209 def filelogrenamed(orig, self, node):
209 def filelogrenamed(orig, self, node):
210 if _islfs(self._revlog, node):
210 if _islfs(self._revlog, node):
211 rawtext = self._revlog.rawdata(node)
211 rawtext = self._revlog.rawdata(node)
212 if not rawtext:
212 if not rawtext:
213 return False
213 return False
214 metadata = pointer.deserialize(rawtext)
214 metadata = pointer.deserialize(rawtext)
215 if b'x-hg-copy' in metadata and b'x-hg-copyrev' in metadata:
215 if b'x-hg-copy' in metadata and b'x-hg-copyrev' in metadata:
216 return metadata[b'x-hg-copy'], bin(metadata[b'x-hg-copyrev'])
216 return metadata[b'x-hg-copy'], bin(metadata[b'x-hg-copyrev'])
217 else:
217 else:
218 return False
218 return False
219 return orig(self, node)
219 return orig(self, node)
220
220
221
221
222 # Wrapping may also be applied by remotefilelog
222 # Wrapping may also be applied by remotefilelog
223 def filelogsize(orig, self, rev):
223 def filelogsize(orig, self, rev):
224 if _islfs(self._revlog, rev=rev):
224 if _islfs(self._revlog, rev=rev):
225 # fast path: use lfs metadata to answer size
225 # fast path: use lfs metadata to answer size
226 rawtext = self._revlog.rawdata(rev)
226 rawtext = self._revlog.rawdata(rev)
227 metadata = pointer.deserialize(rawtext)
227 metadata = pointer.deserialize(rawtext)
228 return int(metadata[b'size'])
228 return int(metadata[b'size'])
229 return orig(self, rev)
229 return orig(self, rev)
230
230
231
231
232 @eh.wrapfunction(revlog, b'_verify_revision')
232 @eh.wrapfunction(revlog, b'_verify_revision')
233 def _verify_revision(orig, rl, skipflags, state, node):
233 def _verify_revision(orig, rl, skipflags, state, node):
234 if _islfs(rl, node=node):
234 if _islfs(rl, node=node):
235 rawtext = rl.rawdata(node)
235 rawtext = rl.rawdata(node)
236 metadata = pointer.deserialize(rawtext)
236 metadata = pointer.deserialize(rawtext)
237
237
238 # Don't skip blobs that are stored locally, as local verification is
238 # Don't skip blobs that are stored locally, as local verification is
239 # relatively cheap and there's no other way to verify the raw data in
239 # relatively cheap and there's no other way to verify the raw data in
240 # the revlog.
240 # the revlog.
241 if rl.opener.lfslocalblobstore.has(metadata.oid()):
241 if rl.opener.lfslocalblobstore.has(metadata.oid()):
242 skipflags &= ~revlog.REVIDX_EXTSTORED
242 skipflags &= ~revlog.REVIDX_EXTSTORED
243 elif skipflags & revlog.REVIDX_EXTSTORED:
243 elif skipflags & revlog.REVIDX_EXTSTORED:
244 # The wrapped method will set `skipread`, but there's enough local
244 # The wrapped method will set `skipread`, but there's enough local
245 # info to check renames.
245 # info to check renames.
246 state[b'safe_renamed'].add(node)
246 state[b'safe_renamed'].add(node)
247
247
248 orig(rl, skipflags, state, node)
248 orig(rl, skipflags, state, node)
249
249
250
250
251 @eh.wrapfunction(context.basefilectx, b'cmp')
251 @eh.wrapfunction(context.basefilectx, b'cmp')
252 def filectxcmp(orig, self, fctx):
252 def filectxcmp(orig, self, fctx):
253 """returns True if text is different than fctx"""
253 """returns True if text is different than fctx"""
254 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
254 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
255 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
255 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
256 # fast path: check LFS oid
256 # fast path: check LFS oid
257 p1 = pointer.deserialize(self.rawdata())
257 p1 = pointer.deserialize(self.rawdata())
258 p2 = pointer.deserialize(fctx.rawdata())
258 p2 = pointer.deserialize(fctx.rawdata())
259 return p1.oid() != p2.oid()
259 return p1.oid() != p2.oid()
260 return orig(self, fctx)
260 return orig(self, fctx)
261
261
262
262
263 @eh.wrapfunction(context.basefilectx, b'isbinary')
263 @eh.wrapfunction(context.basefilectx, b'isbinary')
264 def filectxisbinary(orig, self):
264 def filectxisbinary(orig, self):
265 if self.islfs():
265 if self.islfs():
266 # fast path: use lfs metadata to answer isbinary
266 # fast path: use lfs metadata to answer isbinary
267 metadata = pointer.deserialize(self.rawdata())
267 metadata = pointer.deserialize(self.rawdata())
268 # if lfs metadata says nothing, assume it's binary by default
268 # if lfs metadata says nothing, assume it's binary by default
269 return bool(int(metadata.get(b'x-is-binary', 1)))
269 return bool(int(metadata.get(b'x-is-binary', 1)))
270 return orig(self)
270 return orig(self)
271
271
272
272
273 def filectxislfs(self):
273 def filectxislfs(self):
274 return _islfs(self.filelog()._revlog, self.filenode())
274 return _islfs(self.filelog()._revlog, self.filenode())
275
275
276
276
277 @eh.wrapfunction(cmdutil, b'_updatecatformatter')
277 @eh.wrapfunction(cmdutil, b'_updatecatformatter')
278 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
278 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
279 orig(fm, ctx, matcher, path, decode)
279 orig(fm, ctx, matcher, path, decode)
280 fm.data(rawdata=ctx[path].rawdata())
280 fm.data(rawdata=ctx[path].rawdata())
281
281
282
282
283 @eh.wrapfunction(scmutil, b'wrapconvertsink')
283 @eh.wrapfunction(scmutil, b'wrapconvertsink')
284 def convertsink(orig, sink):
284 def convertsink(orig, sink):
285 sink = orig(sink)
285 sink = orig(sink)
286 if sink.repotype == b'hg':
286 if sink.repotype == b'hg':
287
287
288 class lfssink(sink.__class__):
288 class lfssink(sink.__class__):
289 def putcommit(
289 def putcommit(
290 self,
290 self,
291 files,
291 files,
292 copies,
292 copies,
293 parents,
293 parents,
294 commit,
294 commit,
295 source,
295 source,
296 revmap,
296 revmap,
297 full,
297 full,
298 cleanp2,
298 cleanp2,
299 ):
299 ):
300 pc = super(lfssink, self).putcommit
300 pc = super(lfssink, self).putcommit
301 node = pc(
301 node = pc(
302 files,
302 files,
303 copies,
303 copies,
304 parents,
304 parents,
305 commit,
305 commit,
306 source,
306 source,
307 revmap,
307 revmap,
308 full,
308 full,
309 cleanp2,
309 cleanp2,
310 )
310 )
311
311
312 if b'lfs' not in self.repo.requirements:
312 if b'lfs' not in self.repo.requirements:
313 ctx = self.repo[node]
313 ctx = self.repo[node]
314
314
315 # The file list may contain removed files, so check for
315 # The file list may contain removed files, so check for
316 # membership before assuming it is in the context.
316 # membership before assuming it is in the context.
317 if any(f in ctx and ctx[f].islfs() for f, n in files):
317 if any(f in ctx and ctx[f].islfs() for f, n in files):
318 self.repo.requirements.add(b'lfs')
318 self.repo.requirements.add(b'lfs')
319 scmutil.writereporequirements(self.repo)
319 scmutil.writereporequirements(self.repo)
320
320
321 return node
321 return node
322
322
323 sink.__class__ = lfssink
323 sink.__class__ = lfssink
324
324
325 return sink
325 return sink
326
326
327
327
328 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
328 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
329 # options and blob stores are passed from othervfs to the new readonlyvfs.
329 # options and blob stores are passed from othervfs to the new readonlyvfs.
330 @eh.wrapfunction(vfsmod.readonlyvfs, b'__init__')
330 @eh.wrapfunction(vfsmod.readonlyvfs, b'__init__')
331 def vfsinit(orig, self, othervfs):
331 def vfsinit(orig, self, othervfs):
332 orig(self, othervfs)
332 orig(self, othervfs)
333 # copy lfs related options
333 # copy lfs related options
334 for k, v in othervfs.options.items():
334 for k, v in othervfs.options.items():
335 if k.startswith(b'lfs'):
335 if k.startswith(b'lfs'):
336 self.options[k] = v
336 self.options[k] = v
337 # also copy lfs blobstores. note: this can run before reposetup, so lfs
337 # also copy lfs blobstores. note: this can run before reposetup, so lfs
338 # blobstore attributes are not always ready at this time.
338 # blobstore attributes are not always ready at this time.
339 for name in [b'lfslocalblobstore', b'lfsremoteblobstore']:
339 for name in [b'lfslocalblobstore', b'lfsremoteblobstore']:
340 if util.safehasattr(othervfs, name):
340 if util.safehasattr(othervfs, name):
341 setattr(self, name, getattr(othervfs, name))
341 setattr(self, name, getattr(othervfs, name))
342
342
343
343
344 def _prefetchfiles(repo, revmatches):
344 def _prefetchfiles(repo, revmatches):
345 """Ensure that required LFS blobs are present, fetching them as a group if
345 """Ensure that required LFS blobs are present, fetching them as a group if
346 needed."""
346 needed."""
347 if not util.safehasattr(repo.svfs, b'lfslocalblobstore'):
347 if not util.safehasattr(repo.svfs, b'lfslocalblobstore'):
348 return
348 return
349
349
350 pointers = []
350 pointers = []
351 oids = set()
351 oids = set()
352 localstore = repo.svfs.lfslocalblobstore
352 localstore = repo.svfs.lfslocalblobstore
353
353
354 for rev, match in revmatches:
354 for rev, match in revmatches:
355 ctx = repo[rev]
355 ctx = repo[rev]
356 for f in ctx.walk(match):
356 for f in ctx.walk(match):
357 p = pointerfromctx(ctx, f)
357 p = pointerfromctx(ctx, f)
358 if p and p.oid() not in oids and not localstore.has(p.oid()):
358 if p and p.oid() not in oids and not localstore.has(p.oid()):
359 p.filename = f
359 p.filename = f
360 pointers.append(p)
360 pointers.append(p)
361 oids.add(p.oid())
361 oids.add(p.oid())
362
362
363 if pointers:
363 if pointers:
364 # Recalculating the repo store here allows 'paths.default' that is set
364 # Recalculating the repo store here allows 'paths.default' that is set
365 # on the repo by a clone command to be used for the update.
365 # on the repo by a clone command to be used for the update.
366 blobstore.remote(repo).readbatch(pointers, localstore)
366 blobstore.remote(repo).readbatch(pointers, localstore)
367
367
368
368
369 def _canskipupload(repo):
369 def _canskipupload(repo):
370 # Skip if this hasn't been passed to reposetup()
370 # Skip if this hasn't been passed to reposetup()
371 if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'):
371 if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'):
372 return True
372 return True
373
373
374 # if remotestore is a null store, upload is a no-op and can be skipped
374 # if remotestore is a null store, upload is a no-op and can be skipped
375 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
375 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
376
376
377
377
378 def candownload(repo):
378 def candownload(repo):
379 # Skip if this hasn't been passed to reposetup()
379 # Skip if this hasn't been passed to reposetup()
380 if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'):
380 if not util.safehasattr(repo.svfs, b'lfsremoteblobstore'):
381 return False
381 return False
382
382
383 # if remotestore is a null store, downloads will lead to nothing
383 # if remotestore is a null store, downloads will lead to nothing
384 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
384 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
385
385
386
386
387 def uploadblobsfromrevs(repo, revs):
387 def uploadblobsfromrevs(repo, revs):
388 """upload lfs blobs introduced by revs
388 """upload lfs blobs introduced by revs
389
389
390 Note: also used by other extensions e. g. infinitepush. avoid renaming.
390 Note: also used by other extensions e. g. infinitepush. avoid renaming.
391 """
391 """
392 if _canskipupload(repo):
392 if _canskipupload(repo):
393 return
393 return
394 pointers = extractpointers(repo, revs)
394 pointers = extractpointers(repo, revs)
395 uploadblobs(repo, pointers)
395 uploadblobs(repo, pointers)
396
396
397
397
398 def prepush(pushop):
398 def prepush(pushop):
399 """Prepush hook.
399 """Prepush hook.
400
400
401 Read through the revisions to push, looking for filelog entries that can be
401 Read through the revisions to push, looking for filelog entries that can be
402 deserialized into metadata so that we can block the push on their upload to
402 deserialized into metadata so that we can block the push on their upload to
403 the remote blobstore.
403 the remote blobstore.
404 """
404 """
405 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
405 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
406
406
407
407
408 @eh.wrapfunction(exchange, b'push')
408 @eh.wrapfunction(exchange, b'push')
409 def push(orig, repo, remote, *args, **kwargs):
409 def push(orig, repo, remote, *args, **kwargs):
410 """bail on push if the extension isn't enabled on remote when needed, and
410 """bail on push if the extension isn't enabled on remote when needed, and
411 update the remote store based on the destination path."""
411 update the remote store based on the destination path."""
412 if b'lfs' in repo.requirements:
412 if b'lfs' in repo.requirements:
413 # If the remote peer is for a local repo, the requirement tests in the
413 # If the remote peer is for a local repo, the requirement tests in the
414 # base class method enforce lfs support. Otherwise, some revisions in
414 # base class method enforce lfs support. Otherwise, some revisions in
415 # this repo use lfs, and the remote repo needs the extension loaded.
415 # this repo use lfs, and the remote repo needs the extension loaded.
416 if not remote.local() and not remote.capable(b'lfs'):
416 if not remote.local() and not remote.capable(b'lfs'):
417 # This is a copy of the message in exchange.push() when requirements
417 # This is a copy of the message in exchange.push() when requirements
418 # are missing between local repos.
418 # are missing between local repos.
419 m = _(b"required features are not supported in the destination: %s")
419 m = _(b"required features are not supported in the destination: %s")
420 raise error.Abort(
420 raise error.Abort(
421 m % b'lfs', hint=_(b'enable the lfs extension on the server')
421 m % b'lfs', hint=_(b'enable the lfs extension on the server')
422 )
422 )
423
423
424 # Repositories where this extension is disabled won't have the field.
424 # Repositories where this extension is disabled won't have the field.
425 # But if there's a requirement, then the extension must be loaded AND
425 # But if there's a requirement, then the extension must be loaded AND
426 # there may be blobs to push.
426 # there may be blobs to push.
427 remotestore = repo.svfs.lfsremoteblobstore
427 remotestore = repo.svfs.lfsremoteblobstore
428 try:
428 try:
429 repo.svfs.lfsremoteblobstore = blobstore.remote(repo, remote.url())
429 repo.svfs.lfsremoteblobstore = blobstore.remote(repo, remote.url())
430 return orig(repo, remote, *args, **kwargs)
430 return orig(repo, remote, *args, **kwargs)
431 finally:
431 finally:
432 repo.svfs.lfsremoteblobstore = remotestore
432 repo.svfs.lfsremoteblobstore = remotestore
433 else:
433 else:
434 return orig(repo, remote, *args, **kwargs)
434 return orig(repo, remote, *args, **kwargs)
435
435
436
436
437 # when writing a bundle via "hg bundle" command, upload related LFS blobs
437 # when writing a bundle via "hg bundle" command, upload related LFS blobs
438 @eh.wrapfunction(bundle2, b'writenewbundle')
438 @eh.wrapfunction(bundle2, b'writenewbundle')
439 def writenewbundle(
439 def writenewbundle(
440 orig, ui, repo, source, filename, bundletype, outgoing, *args, **kwargs
440 orig, ui, repo, source, filename, bundletype, outgoing, *args, **kwargs
441 ):
441 ):
442 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
442 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
443 uploadblobsfromrevs(repo, outgoing.missing)
443 uploadblobsfromrevs(repo, outgoing.missing)
444 return orig(
444 return orig(
445 ui, repo, source, filename, bundletype, outgoing, *args, **kwargs
445 ui, repo, source, filename, bundletype, outgoing, *args, **kwargs
446 )
446 )
447
447
448
448
449 def extractpointers(repo, revs):
449 def extractpointers(repo, revs):
450 """return a list of lfs pointers added by given revs"""
450 """return a list of lfs pointers added by given revs"""
451 repo.ui.debug(b'lfs: computing set of blobs to upload\n')
451 repo.ui.debug(b'lfs: computing set of blobs to upload\n')
452 pointers = {}
452 pointers = {}
453
453
454 makeprogress = repo.ui.makeprogress
454 makeprogress = repo.ui.makeprogress
455 with makeprogress(
455 with makeprogress(
456 _(b'lfs search'), _(b'changesets'), len(revs)
456 _(b'lfs search'), _(b'changesets'), len(revs)
457 ) as progress:
457 ) as progress:
458 for r in revs:
458 for r in revs:
459 ctx = repo[r]
459 ctx = repo[r]
460 for p in pointersfromctx(ctx).values():
460 for p in pointersfromctx(ctx).values():
461 pointers[p.oid()] = p
461 pointers[p.oid()] = p
462 progress.increment()
462 progress.increment()
463 return sorted(pointers.values(), key=lambda p: p.oid())
463 return sorted(pointers.values(), key=lambda p: p.oid())
464
464
465
465
466 def pointerfromctx(ctx, f, removed=False):
466 def pointerfromctx(ctx, f, removed=False):
467 """return a pointer for the named file from the given changectx, or None if
467 """return a pointer for the named file from the given changectx, or None if
468 the file isn't LFS.
468 the file isn't LFS.
469
469
470 Optionally, the pointer for a file deleted from the context can be returned.
470 Optionally, the pointer for a file deleted from the context can be returned.
471 Since no such pointer is actually stored, and to distinguish from a non LFS
471 Since no such pointer is actually stored, and to distinguish from a non LFS
472 file, this pointer is represented by an empty dict.
472 file, this pointer is represented by an empty dict.
473 """
473 """
474 _ctx = ctx
474 _ctx = ctx
475 if f not in ctx:
475 if f not in ctx:
476 if not removed:
476 if not removed:
477 return None
477 return None
478 if f in ctx.p1():
478 if f in ctx.p1():
479 _ctx = ctx.p1()
479 _ctx = ctx.p1()
480 elif f in ctx.p2():
480 elif f in ctx.p2():
481 _ctx = ctx.p2()
481 _ctx = ctx.p2()
482 else:
482 else:
483 return None
483 return None
484 fctx = _ctx[f]
484 fctx = _ctx[f]
485 if not _islfs(fctx.filelog()._revlog, fctx.filenode()):
485 if not _islfs(fctx.filelog()._revlog, fctx.filenode()):
486 return None
486 return None
487 try:
487 try:
488 p = pointer.deserialize(fctx.rawdata())
488 p = pointer.deserialize(fctx.rawdata())
489 if ctx == _ctx:
489 if ctx == _ctx:
490 return p
490 return p
491 return {}
491 return {}
492 except pointer.InvalidPointer as ex:
492 except pointer.InvalidPointer as ex:
493 raise error.Abort(
493 raise error.Abort(
494 _(b'lfs: corrupted pointer (%s@%s): %s\n')
494 _(b'lfs: corrupted pointer (%s@%s): %s\n')
495 % (f, short(_ctx.node()), ex)
495 % (f, short(_ctx.node()), ex)
496 )
496 )
497
497
498
498
499 def pointersfromctx(ctx, removed=False):
499 def pointersfromctx(ctx, removed=False):
500 """return a dict {path: pointer} for given single changectx.
500 """return a dict {path: pointer} for given single changectx.
501
501
502 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
502 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
503 stored for the path is an empty dict.
503 stored for the path is an empty dict.
504 """
504 """
505 result = {}
505 result = {}
506 m = ctx.repo().narrowmatch()
506 m = ctx.repo().narrowmatch()
507
507
508 # TODO: consider manifest.fastread() instead
508 # TODO: consider manifest.fastread() instead
509 for f in ctx.files():
509 for f in ctx.files():
510 if not m(f):
510 if not m(f):
511 continue
511 continue
512 p = pointerfromctx(ctx, f, removed=removed)
512 p = pointerfromctx(ctx, f, removed=removed)
513 if p is not None:
513 if p is not None:
514 result[f] = p
514 result[f] = p
515 return result
515 return result
516
516
517
517
518 def uploadblobs(repo, pointers):
518 def uploadblobs(repo, pointers):
519 """upload given pointers from local blobstore"""
519 """upload given pointers from local blobstore"""
520 if not pointers:
520 if not pointers:
521 return
521 return
522
522
523 remoteblob = repo.svfs.lfsremoteblobstore
523 remoteblob = repo.svfs.lfsremoteblobstore
524 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
524 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
525
525
526
526
527 @eh.wrapfunction(upgrade_engine, b'_finishdatamigration')
527 @eh.wrapfunction(upgrade_engine, b'finishdatamigration')
528 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
528 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
529 orig(ui, srcrepo, dstrepo, requirements)
529 orig(ui, srcrepo, dstrepo, requirements)
530
530
531 # Skip if this hasn't been passed to reposetup()
531 # Skip if this hasn't been passed to reposetup()
532 if util.safehasattr(
532 if util.safehasattr(
533 srcrepo.svfs, b'lfslocalblobstore'
533 srcrepo.svfs, b'lfslocalblobstore'
534 ) and util.safehasattr(dstrepo.svfs, b'lfslocalblobstore'):
534 ) and util.safehasattr(dstrepo.svfs, b'lfslocalblobstore'):
535 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
535 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
536 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
536 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
537
537
538 for dirpath, dirs, files in srclfsvfs.walk():
538 for dirpath, dirs, files in srclfsvfs.walk():
539 for oid in files:
539 for oid in files:
540 ui.write(_(b'copying lfs blob %s\n') % oid)
540 ui.write(_(b'copying lfs blob %s\n') % oid)
541 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
541 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
542
542
543
543
544 @eh.wrapfunction(upgrade_actions, b'preservedrequirements')
544 @eh.wrapfunction(upgrade_actions, b'preservedrequirements')
545 @eh.wrapfunction(upgrade_actions, b'supporteddestrequirements')
545 @eh.wrapfunction(upgrade_actions, b'supporteddestrequirements')
546 def upgraderequirements(orig, repo):
546 def upgraderequirements(orig, repo):
547 reqs = orig(repo)
547 reqs = orig(repo)
548 if b'lfs' in repo.requirements:
548 if b'lfs' in repo.requirements:
549 reqs.add(b'lfs')
549 reqs.add(b'lfs')
550 return reqs
550 return reqs
@@ -1,520 +1,520 b''
1 # upgrade.py - functions for in place upgrade of Mercurial repository
1 # upgrade.py - functions for in place upgrade of Mercurial repository
2 #
2 #
3 # Copyright (c) 2016-present, Gregory Szorc
3 # Copyright (c) 2016-present, Gregory Szorc
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 stat
10 import stat
11
11
12 from ..i18n import _
12 from ..i18n import _
13 from ..pycompat import getattr
13 from ..pycompat import getattr
14 from .. import (
14 from .. import (
15 changelog,
15 changelog,
16 error,
16 error,
17 filelog,
17 filelog,
18 manifest,
18 manifest,
19 metadata,
19 metadata,
20 pycompat,
20 pycompat,
21 requirements,
21 requirements,
22 revlog,
22 revlog,
23 scmutil,
23 scmutil,
24 util,
24 util,
25 vfs as vfsmod,
25 vfs as vfsmod,
26 )
26 )
27
27
28
28
29 def _revlogfrompath(repo, path):
29 def _revlogfrompath(repo, path):
30 """Obtain a revlog from a repo path.
30 """Obtain a revlog from a repo path.
31
31
32 An instance of the appropriate class is returned.
32 An instance of the appropriate class is returned.
33 """
33 """
34 if path == b'00changelog.i':
34 if path == b'00changelog.i':
35 return changelog.changelog(repo.svfs)
35 return changelog.changelog(repo.svfs)
36 elif path.endswith(b'00manifest.i'):
36 elif path.endswith(b'00manifest.i'):
37 mandir = path[: -len(b'00manifest.i')]
37 mandir = path[: -len(b'00manifest.i')]
38 return manifest.manifestrevlog(repo.svfs, tree=mandir)
38 return manifest.manifestrevlog(repo.svfs, tree=mandir)
39 else:
39 else:
40 # reverse of "/".join(("data", path + ".i"))
40 # reverse of "/".join(("data", path + ".i"))
41 return filelog.filelog(repo.svfs, path[5:-2])
41 return filelog.filelog(repo.svfs, path[5:-2])
42
42
43
43
44 def _copyrevlog(tr, destrepo, oldrl, unencodedname):
44 def _copyrevlog(tr, destrepo, oldrl, unencodedname):
45 """copy all relevant files for `oldrl` into `destrepo` store
45 """copy all relevant files for `oldrl` into `destrepo` store
46
46
47 Files are copied "as is" without any transformation. The copy is performed
47 Files are copied "as is" without any transformation. The copy is performed
48 without extra checks. Callers are responsible for making sure the copied
48 without extra checks. Callers are responsible for making sure the copied
49 content is compatible with format of the destination repository.
49 content is compatible with format of the destination repository.
50 """
50 """
51 oldrl = getattr(oldrl, '_revlog', oldrl)
51 oldrl = getattr(oldrl, '_revlog', oldrl)
52 newrl = _revlogfrompath(destrepo, unencodedname)
52 newrl = _revlogfrompath(destrepo, unencodedname)
53 newrl = getattr(newrl, '_revlog', newrl)
53 newrl = getattr(newrl, '_revlog', newrl)
54
54
55 oldvfs = oldrl.opener
55 oldvfs = oldrl.opener
56 newvfs = newrl.opener
56 newvfs = newrl.opener
57 oldindex = oldvfs.join(oldrl.indexfile)
57 oldindex = oldvfs.join(oldrl.indexfile)
58 newindex = newvfs.join(newrl.indexfile)
58 newindex = newvfs.join(newrl.indexfile)
59 olddata = oldvfs.join(oldrl.datafile)
59 olddata = oldvfs.join(oldrl.datafile)
60 newdata = newvfs.join(newrl.datafile)
60 newdata = newvfs.join(newrl.datafile)
61
61
62 with newvfs(newrl.indexfile, b'w'):
62 with newvfs(newrl.indexfile, b'w'):
63 pass # create all the directories
63 pass # create all the directories
64
64
65 util.copyfile(oldindex, newindex)
65 util.copyfile(oldindex, newindex)
66 copydata = oldrl.opener.exists(oldrl.datafile)
66 copydata = oldrl.opener.exists(oldrl.datafile)
67 if copydata:
67 if copydata:
68 util.copyfile(olddata, newdata)
68 util.copyfile(olddata, newdata)
69
69
70 if not (
70 if not (
71 unencodedname.endswith(b'00changelog.i')
71 unencodedname.endswith(b'00changelog.i')
72 or unencodedname.endswith(b'00manifest.i')
72 or unencodedname.endswith(b'00manifest.i')
73 ):
73 ):
74 destrepo.svfs.fncache.add(unencodedname)
74 destrepo.svfs.fncache.add(unencodedname)
75 if copydata:
75 if copydata:
76 destrepo.svfs.fncache.add(unencodedname[:-2] + b'.d')
76 destrepo.svfs.fncache.add(unencodedname[:-2] + b'.d')
77
77
78
78
79 UPGRADE_CHANGELOG = b"changelog"
79 UPGRADE_CHANGELOG = b"changelog"
80 UPGRADE_MANIFEST = b"manifest"
80 UPGRADE_MANIFEST = b"manifest"
81 UPGRADE_FILELOGS = b"all-filelogs"
81 UPGRADE_FILELOGS = b"all-filelogs"
82
82
83 UPGRADE_ALL_REVLOGS = frozenset(
83 UPGRADE_ALL_REVLOGS = frozenset(
84 [UPGRADE_CHANGELOG, UPGRADE_MANIFEST, UPGRADE_FILELOGS]
84 [UPGRADE_CHANGELOG, UPGRADE_MANIFEST, UPGRADE_FILELOGS]
85 )
85 )
86
86
87
87
88 def getsidedatacompanion(srcrepo, dstrepo):
88 def getsidedatacompanion(srcrepo, dstrepo):
89 sidedatacompanion = None
89 sidedatacompanion = None
90 removedreqs = srcrepo.requirements - dstrepo.requirements
90 removedreqs = srcrepo.requirements - dstrepo.requirements
91 addedreqs = dstrepo.requirements - srcrepo.requirements
91 addedreqs = dstrepo.requirements - srcrepo.requirements
92 if requirements.SIDEDATA_REQUIREMENT in removedreqs:
92 if requirements.SIDEDATA_REQUIREMENT in removedreqs:
93
93
94 def sidedatacompanion(rl, rev):
94 def sidedatacompanion(rl, rev):
95 rl = getattr(rl, '_revlog', rl)
95 rl = getattr(rl, '_revlog', rl)
96 if rl.flags(rev) & revlog.REVIDX_SIDEDATA:
96 if rl.flags(rev) & revlog.REVIDX_SIDEDATA:
97 return True, (), {}, 0, 0
97 return True, (), {}, 0, 0
98 return False, (), {}, 0, 0
98 return False, (), {}, 0, 0
99
99
100 elif requirements.COPIESSDC_REQUIREMENT in addedreqs:
100 elif requirements.COPIESSDC_REQUIREMENT in addedreqs:
101 sidedatacompanion = metadata.getsidedataadder(srcrepo, dstrepo)
101 sidedatacompanion = metadata.getsidedataadder(srcrepo, dstrepo)
102 elif requirements.COPIESSDC_REQUIREMENT in removedreqs:
102 elif requirements.COPIESSDC_REQUIREMENT in removedreqs:
103 sidedatacompanion = metadata.getsidedataremover(srcrepo, dstrepo)
103 sidedatacompanion = metadata.getsidedataremover(srcrepo, dstrepo)
104 return sidedatacompanion
104 return sidedatacompanion
105
105
106
106
107 def matchrevlog(revlogfilter, entry):
107 def matchrevlog(revlogfilter, entry):
108 """check if a revlog is selected for cloning.
108 """check if a revlog is selected for cloning.
109
109
110 In other words, are there any updates which need to be done on revlog
110 In other words, are there any updates which need to be done on revlog
111 or it can be blindly copied.
111 or it can be blindly copied.
112
112
113 The store entry is checked against the passed filter"""
113 The store entry is checked against the passed filter"""
114 if entry.endswith(b'00changelog.i'):
114 if entry.endswith(b'00changelog.i'):
115 return UPGRADE_CHANGELOG in revlogfilter
115 return UPGRADE_CHANGELOG in revlogfilter
116 elif entry.endswith(b'00manifest.i'):
116 elif entry.endswith(b'00manifest.i'):
117 return UPGRADE_MANIFEST in revlogfilter
117 return UPGRADE_MANIFEST in revlogfilter
118 return UPGRADE_FILELOGS in revlogfilter
118 return UPGRADE_FILELOGS in revlogfilter
119
119
120
120
121 def _perform_clone(
121 def _perform_clone(
122 ui,
122 ui,
123 dstrepo,
123 dstrepo,
124 tr,
124 tr,
125 old_revlog,
125 old_revlog,
126 unencoded,
126 unencoded,
127 upgrade_op,
127 upgrade_op,
128 sidedatacompanion,
128 sidedatacompanion,
129 oncopiedrevision,
129 oncopiedrevision,
130 ):
130 ):
131 """ returns the new revlog object created"""
131 """ returns the new revlog object created"""
132 newrl = None
132 newrl = None
133 if matchrevlog(upgrade_op.revlogs_to_process, unencoded):
133 if matchrevlog(upgrade_op.revlogs_to_process, unencoded):
134 ui.note(
134 ui.note(
135 _(b'cloning %d revisions from %s\n') % (len(old_revlog), unencoded)
135 _(b'cloning %d revisions from %s\n') % (len(old_revlog), unencoded)
136 )
136 )
137 newrl = _revlogfrompath(dstrepo, unencoded)
137 newrl = _revlogfrompath(dstrepo, unencoded)
138 old_revlog.clone(
138 old_revlog.clone(
139 tr,
139 tr,
140 newrl,
140 newrl,
141 addrevisioncb=oncopiedrevision,
141 addrevisioncb=oncopiedrevision,
142 deltareuse=upgrade_op.delta_reuse_mode,
142 deltareuse=upgrade_op.delta_reuse_mode,
143 forcedeltabothparents=upgrade_op.force_re_delta_both_parents,
143 forcedeltabothparents=upgrade_op.force_re_delta_both_parents,
144 sidedatacompanion=sidedatacompanion,
144 sidedatacompanion=sidedatacompanion,
145 )
145 )
146 else:
146 else:
147 msg = _(b'blindly copying %s containing %i revisions\n')
147 msg = _(b'blindly copying %s containing %i revisions\n')
148 ui.note(msg % (unencoded, len(old_revlog)))
148 ui.note(msg % (unencoded, len(old_revlog)))
149 _copyrevlog(tr, dstrepo, old_revlog, unencoded)
149 _copyrevlog(tr, dstrepo, old_revlog, unencoded)
150
150
151 newrl = _revlogfrompath(dstrepo, unencoded)
151 newrl = _revlogfrompath(dstrepo, unencoded)
152 return newrl
152 return newrl
153
153
154
154
155 def _clonerevlogs(
155 def _clonerevlogs(
156 ui,
156 ui,
157 srcrepo,
157 srcrepo,
158 dstrepo,
158 dstrepo,
159 tr,
159 tr,
160 upgrade_op,
160 upgrade_op,
161 ):
161 ):
162 """Copy revlogs between 2 repos."""
162 """Copy revlogs between 2 repos."""
163 revcount = 0
163 revcount = 0
164 srcsize = 0
164 srcsize = 0
165 srcrawsize = 0
165 srcrawsize = 0
166 dstsize = 0
166 dstsize = 0
167 fcount = 0
167 fcount = 0
168 frevcount = 0
168 frevcount = 0
169 fsrcsize = 0
169 fsrcsize = 0
170 frawsize = 0
170 frawsize = 0
171 fdstsize = 0
171 fdstsize = 0
172 mcount = 0
172 mcount = 0
173 mrevcount = 0
173 mrevcount = 0
174 msrcsize = 0
174 msrcsize = 0
175 mrawsize = 0
175 mrawsize = 0
176 mdstsize = 0
176 mdstsize = 0
177 crevcount = 0
177 crevcount = 0
178 csrcsize = 0
178 csrcsize = 0
179 crawsize = 0
179 crawsize = 0
180 cdstsize = 0
180 cdstsize = 0
181
181
182 alldatafiles = list(srcrepo.store.walk())
182 alldatafiles = list(srcrepo.store.walk())
183 # mapping of data files which needs to be cloned
183 # mapping of data files which needs to be cloned
184 # key is unencoded filename
184 # key is unencoded filename
185 # value is revlog_object_from_srcrepo
185 # value is revlog_object_from_srcrepo
186 manifests = {}
186 manifests = {}
187 changelogs = {}
187 changelogs = {}
188 filelogs = {}
188 filelogs = {}
189
189
190 # Perform a pass to collect metadata. This validates we can open all
190 # Perform a pass to collect metadata. This validates we can open all
191 # source files and allows a unified progress bar to be displayed.
191 # source files and allows a unified progress bar to be displayed.
192 for unencoded, encoded, size in alldatafiles:
192 for unencoded, encoded, size in alldatafiles:
193 if unencoded.endswith(b'.d'):
193 if unencoded.endswith(b'.d'):
194 continue
194 continue
195
195
196 rl = _revlogfrompath(srcrepo, unencoded)
196 rl = _revlogfrompath(srcrepo, unencoded)
197
197
198 info = rl.storageinfo(
198 info = rl.storageinfo(
199 exclusivefiles=True,
199 exclusivefiles=True,
200 revisionscount=True,
200 revisionscount=True,
201 trackedsize=True,
201 trackedsize=True,
202 storedsize=True,
202 storedsize=True,
203 )
203 )
204
204
205 revcount += info[b'revisionscount'] or 0
205 revcount += info[b'revisionscount'] or 0
206 datasize = info[b'storedsize'] or 0
206 datasize = info[b'storedsize'] or 0
207 rawsize = info[b'trackedsize'] or 0
207 rawsize = info[b'trackedsize'] or 0
208
208
209 srcsize += datasize
209 srcsize += datasize
210 srcrawsize += rawsize
210 srcrawsize += rawsize
211
211
212 # This is for the separate progress bars.
212 # This is for the separate progress bars.
213 if isinstance(rl, changelog.changelog):
213 if isinstance(rl, changelog.changelog):
214 changelogs[unencoded] = rl
214 changelogs[unencoded] = rl
215 crevcount += len(rl)
215 crevcount += len(rl)
216 csrcsize += datasize
216 csrcsize += datasize
217 crawsize += rawsize
217 crawsize += rawsize
218 elif isinstance(rl, manifest.manifestrevlog):
218 elif isinstance(rl, manifest.manifestrevlog):
219 manifests[unencoded] = rl
219 manifests[unencoded] = rl
220 mcount += 1
220 mcount += 1
221 mrevcount += len(rl)
221 mrevcount += len(rl)
222 msrcsize += datasize
222 msrcsize += datasize
223 mrawsize += rawsize
223 mrawsize += rawsize
224 elif isinstance(rl, filelog.filelog):
224 elif isinstance(rl, filelog.filelog):
225 filelogs[unencoded] = rl
225 filelogs[unencoded] = rl
226 fcount += 1
226 fcount += 1
227 frevcount += len(rl)
227 frevcount += len(rl)
228 fsrcsize += datasize
228 fsrcsize += datasize
229 frawsize += rawsize
229 frawsize += rawsize
230 else:
230 else:
231 error.ProgrammingError(b'unknown revlog type')
231 error.ProgrammingError(b'unknown revlog type')
232
232
233 if not revcount:
233 if not revcount:
234 return
234 return
235
235
236 ui.status(
236 ui.status(
237 _(
237 _(
238 b'migrating %d total revisions (%d in filelogs, %d in manifests, '
238 b'migrating %d total revisions (%d in filelogs, %d in manifests, '
239 b'%d in changelog)\n'
239 b'%d in changelog)\n'
240 )
240 )
241 % (revcount, frevcount, mrevcount, crevcount)
241 % (revcount, frevcount, mrevcount, crevcount)
242 )
242 )
243 ui.status(
243 ui.status(
244 _(b'migrating %s in store; %s tracked data\n')
244 _(b'migrating %s in store; %s tracked data\n')
245 % ((util.bytecount(srcsize), util.bytecount(srcrawsize)))
245 % ((util.bytecount(srcsize), util.bytecount(srcrawsize)))
246 )
246 )
247
247
248 # Used to keep track of progress.
248 # Used to keep track of progress.
249 progress = None
249 progress = None
250
250
251 def oncopiedrevision(rl, rev, node):
251 def oncopiedrevision(rl, rev, node):
252 progress.increment()
252 progress.increment()
253
253
254 sidedatacompanion = getsidedatacompanion(srcrepo, dstrepo)
254 sidedatacompanion = getsidedatacompanion(srcrepo, dstrepo)
255
255
256 # Migrating filelogs
256 # Migrating filelogs
257 ui.status(
257 ui.status(
258 _(
258 _(
259 b'migrating %d filelogs containing %d revisions '
259 b'migrating %d filelogs containing %d revisions '
260 b'(%s in store; %s tracked data)\n'
260 b'(%s in store; %s tracked data)\n'
261 )
261 )
262 % (
262 % (
263 fcount,
263 fcount,
264 frevcount,
264 frevcount,
265 util.bytecount(fsrcsize),
265 util.bytecount(fsrcsize),
266 util.bytecount(frawsize),
266 util.bytecount(frawsize),
267 )
267 )
268 )
268 )
269 progress = srcrepo.ui.makeprogress(_(b'file revisions'), total=frevcount)
269 progress = srcrepo.ui.makeprogress(_(b'file revisions'), total=frevcount)
270 for unencoded, oldrl in sorted(filelogs.items()):
270 for unencoded, oldrl in sorted(filelogs.items()):
271 newrl = _perform_clone(
271 newrl = _perform_clone(
272 ui,
272 ui,
273 dstrepo,
273 dstrepo,
274 tr,
274 tr,
275 oldrl,
275 oldrl,
276 unencoded,
276 unencoded,
277 upgrade_op,
277 upgrade_op,
278 sidedatacompanion,
278 sidedatacompanion,
279 oncopiedrevision,
279 oncopiedrevision,
280 )
280 )
281 info = newrl.storageinfo(storedsize=True)
281 info = newrl.storageinfo(storedsize=True)
282 fdstsize += info[b'storedsize'] or 0
282 fdstsize += info[b'storedsize'] or 0
283 ui.status(
283 ui.status(
284 _(
284 _(
285 b'finished migrating %d filelog revisions across %d '
285 b'finished migrating %d filelog revisions across %d '
286 b'filelogs; change in size: %s\n'
286 b'filelogs; change in size: %s\n'
287 )
287 )
288 % (frevcount, fcount, util.bytecount(fdstsize - fsrcsize))
288 % (frevcount, fcount, util.bytecount(fdstsize - fsrcsize))
289 )
289 )
290
290
291 # Migrating manifests
291 # Migrating manifests
292 ui.status(
292 ui.status(
293 _(
293 _(
294 b'migrating %d manifests containing %d revisions '
294 b'migrating %d manifests containing %d revisions '
295 b'(%s in store; %s tracked data)\n'
295 b'(%s in store; %s tracked data)\n'
296 )
296 )
297 % (
297 % (
298 mcount,
298 mcount,
299 mrevcount,
299 mrevcount,
300 util.bytecount(msrcsize),
300 util.bytecount(msrcsize),
301 util.bytecount(mrawsize),
301 util.bytecount(mrawsize),
302 )
302 )
303 )
303 )
304 if progress:
304 if progress:
305 progress.complete()
305 progress.complete()
306 progress = srcrepo.ui.makeprogress(
306 progress = srcrepo.ui.makeprogress(
307 _(b'manifest revisions'), total=mrevcount
307 _(b'manifest revisions'), total=mrevcount
308 )
308 )
309 for unencoded, oldrl in sorted(manifests.items()):
309 for unencoded, oldrl in sorted(manifests.items()):
310 newrl = _perform_clone(
310 newrl = _perform_clone(
311 ui,
311 ui,
312 dstrepo,
312 dstrepo,
313 tr,
313 tr,
314 oldrl,
314 oldrl,
315 unencoded,
315 unencoded,
316 upgrade_op,
316 upgrade_op,
317 sidedatacompanion,
317 sidedatacompanion,
318 oncopiedrevision,
318 oncopiedrevision,
319 )
319 )
320 info = newrl.storageinfo(storedsize=True)
320 info = newrl.storageinfo(storedsize=True)
321 mdstsize += info[b'storedsize'] or 0
321 mdstsize += info[b'storedsize'] or 0
322 ui.status(
322 ui.status(
323 _(
323 _(
324 b'finished migrating %d manifest revisions across %d '
324 b'finished migrating %d manifest revisions across %d '
325 b'manifests; change in size: %s\n'
325 b'manifests; change in size: %s\n'
326 )
326 )
327 % (mrevcount, mcount, util.bytecount(mdstsize - msrcsize))
327 % (mrevcount, mcount, util.bytecount(mdstsize - msrcsize))
328 )
328 )
329
329
330 # Migrating changelog
330 # Migrating changelog
331 ui.status(
331 ui.status(
332 _(
332 _(
333 b'migrating changelog containing %d revisions '
333 b'migrating changelog containing %d revisions '
334 b'(%s in store; %s tracked data)\n'
334 b'(%s in store; %s tracked data)\n'
335 )
335 )
336 % (
336 % (
337 crevcount,
337 crevcount,
338 util.bytecount(csrcsize),
338 util.bytecount(csrcsize),
339 util.bytecount(crawsize),
339 util.bytecount(crawsize),
340 )
340 )
341 )
341 )
342 if progress:
342 if progress:
343 progress.complete()
343 progress.complete()
344 progress = srcrepo.ui.makeprogress(
344 progress = srcrepo.ui.makeprogress(
345 _(b'changelog revisions'), total=crevcount
345 _(b'changelog revisions'), total=crevcount
346 )
346 )
347 for unencoded, oldrl in sorted(changelogs.items()):
347 for unencoded, oldrl in sorted(changelogs.items()):
348 newrl = _perform_clone(
348 newrl = _perform_clone(
349 ui,
349 ui,
350 dstrepo,
350 dstrepo,
351 tr,
351 tr,
352 oldrl,
352 oldrl,
353 unencoded,
353 unencoded,
354 upgrade_op,
354 upgrade_op,
355 sidedatacompanion,
355 sidedatacompanion,
356 oncopiedrevision,
356 oncopiedrevision,
357 )
357 )
358 info = newrl.storageinfo(storedsize=True)
358 info = newrl.storageinfo(storedsize=True)
359 cdstsize += info[b'storedsize'] or 0
359 cdstsize += info[b'storedsize'] or 0
360 progress.complete()
360 progress.complete()
361 ui.status(
361 ui.status(
362 _(
362 _(
363 b'finished migrating %d changelog revisions; change in size: '
363 b'finished migrating %d changelog revisions; change in size: '
364 b'%s\n'
364 b'%s\n'
365 )
365 )
366 % (crevcount, util.bytecount(cdstsize - csrcsize))
366 % (crevcount, util.bytecount(cdstsize - csrcsize))
367 )
367 )
368
368
369 dstsize = fdstsize + mdstsize + cdstsize
369 dstsize = fdstsize + mdstsize + cdstsize
370 ui.status(
370 ui.status(
371 _(
371 _(
372 b'finished migrating %d total revisions; total change in store '
372 b'finished migrating %d total revisions; total change in store '
373 b'size: %s\n'
373 b'size: %s\n'
374 )
374 )
375 % (revcount, util.bytecount(dstsize - srcsize))
375 % (revcount, util.bytecount(dstsize - srcsize))
376 )
376 )
377
377
378
378
379 def _filterstorefile(srcrepo, dstrepo, requirements, path, mode, st):
379 def _filterstorefile(srcrepo, dstrepo, requirements, path, mode, st):
380 """Determine whether to copy a store file during upgrade.
380 """Determine whether to copy a store file during upgrade.
381
381
382 This function is called when migrating store files from ``srcrepo`` to
382 This function is called when migrating store files from ``srcrepo`` to
383 ``dstrepo`` as part of upgrading a repository.
383 ``dstrepo`` as part of upgrading a repository.
384
384
385 Args:
385 Args:
386 srcrepo: repo we are copying from
386 srcrepo: repo we are copying from
387 dstrepo: repo we are copying to
387 dstrepo: repo we are copying to
388 requirements: set of requirements for ``dstrepo``
388 requirements: set of requirements for ``dstrepo``
389 path: store file being examined
389 path: store file being examined
390 mode: the ``ST_MODE`` file type of ``path``
390 mode: the ``ST_MODE`` file type of ``path``
391 st: ``stat`` data structure for ``path``
391 st: ``stat`` data structure for ``path``
392
392
393 Function should return ``True`` if the file is to be copied.
393 Function should return ``True`` if the file is to be copied.
394 """
394 """
395 # Skip revlogs.
395 # Skip revlogs.
396 if path.endswith((b'.i', b'.d', b'.n', b'.nd')):
396 if path.endswith((b'.i', b'.d', b'.n', b'.nd')):
397 return False
397 return False
398 # Skip transaction related files.
398 # Skip transaction related files.
399 if path.startswith(b'undo'):
399 if path.startswith(b'undo'):
400 return False
400 return False
401 # Only copy regular files.
401 # Only copy regular files.
402 if mode != stat.S_IFREG:
402 if mode != stat.S_IFREG:
403 return False
403 return False
404 # Skip other skipped files.
404 # Skip other skipped files.
405 if path in (b'lock', b'fncache'):
405 if path in (b'lock', b'fncache'):
406 return False
406 return False
407
407
408 return True
408 return True
409
409
410
410
411 def _finishdatamigration(ui, srcrepo, dstrepo, requirements):
411 def finishdatamigration(ui, srcrepo, dstrepo, requirements):
412 """Hook point for extensions to perform additional actions during upgrade.
412 """Hook point for extensions to perform additional actions during upgrade.
413
413
414 This function is called after revlogs and store files have been copied but
414 This function is called after revlogs and store files have been copied but
415 before the new store is swapped into the original location.
415 before the new store is swapped into the original location.
416 """
416 """
417
417
418
418
419 def upgrade(ui, srcrepo, dstrepo, upgrade_op):
419 def upgrade(ui, srcrepo, dstrepo, upgrade_op):
420 """Do the low-level work of upgrading a repository.
420 """Do the low-level work of upgrading a repository.
421
421
422 The upgrade is effectively performed as a copy between a source
422 The upgrade is effectively performed as a copy between a source
423 repository and a temporary destination repository.
423 repository and a temporary destination repository.
424
424
425 The source repository is unmodified for as long as possible so the
425 The source repository is unmodified for as long as possible so the
426 upgrade can abort at any time without causing loss of service for
426 upgrade can abort at any time without causing loss of service for
427 readers and without corrupting the source repository.
427 readers and without corrupting the source repository.
428 """
428 """
429 assert srcrepo.currentwlock()
429 assert srcrepo.currentwlock()
430 assert dstrepo.currentwlock()
430 assert dstrepo.currentwlock()
431
431
432 ui.status(
432 ui.status(
433 _(
433 _(
434 b'(it is safe to interrupt this process any time before '
434 b'(it is safe to interrupt this process any time before '
435 b'data migration completes)\n'
435 b'data migration completes)\n'
436 )
436 )
437 )
437 )
438
438
439 with dstrepo.transaction(b'upgrade') as tr:
439 with dstrepo.transaction(b'upgrade') as tr:
440 _clonerevlogs(
440 _clonerevlogs(
441 ui,
441 ui,
442 srcrepo,
442 srcrepo,
443 dstrepo,
443 dstrepo,
444 tr,
444 tr,
445 upgrade_op,
445 upgrade_op,
446 )
446 )
447
447
448 # Now copy other files in the store directory.
448 # Now copy other files in the store directory.
449 # The sorted() makes execution deterministic.
449 # The sorted() makes execution deterministic.
450 for p, kind, st in sorted(srcrepo.store.vfs.readdir(b'', stat=True)):
450 for p, kind, st in sorted(srcrepo.store.vfs.readdir(b'', stat=True)):
451 if not _filterstorefile(
451 if not _filterstorefile(
452 srcrepo, dstrepo, upgrade_op.new_requirements, p, kind, st
452 srcrepo, dstrepo, upgrade_op.new_requirements, p, kind, st
453 ):
453 ):
454 continue
454 continue
455
455
456 srcrepo.ui.status(_(b'copying %s\n') % p)
456 srcrepo.ui.status(_(b'copying %s\n') % p)
457 src = srcrepo.store.rawvfs.join(p)
457 src = srcrepo.store.rawvfs.join(p)
458 dst = dstrepo.store.rawvfs.join(p)
458 dst = dstrepo.store.rawvfs.join(p)
459 util.copyfile(src, dst, copystat=True)
459 util.copyfile(src, dst, copystat=True)
460
460
461 _finishdatamigration(ui, srcrepo, dstrepo, requirements)
461 finishdatamigration(ui, srcrepo, dstrepo, requirements)
462
462
463 ui.status(_(b'data fully migrated to temporary repository\n'))
463 ui.status(_(b'data fully migrated to temporary repository\n'))
464
464
465 backuppath = pycompat.mkdtemp(prefix=b'upgradebackup.', dir=srcrepo.path)
465 backuppath = pycompat.mkdtemp(prefix=b'upgradebackup.', dir=srcrepo.path)
466 backupvfs = vfsmod.vfs(backuppath)
466 backupvfs = vfsmod.vfs(backuppath)
467
467
468 # Make a backup of requires file first, as it is the first to be modified.
468 # Make a backup of requires file first, as it is the first to be modified.
469 util.copyfile(srcrepo.vfs.join(b'requires'), backupvfs.join(b'requires'))
469 util.copyfile(srcrepo.vfs.join(b'requires'), backupvfs.join(b'requires'))
470
470
471 # We install an arbitrary requirement that clients must not support
471 # We install an arbitrary requirement that clients must not support
472 # as a mechanism to lock out new clients during the data swap. This is
472 # as a mechanism to lock out new clients during the data swap. This is
473 # better than allowing a client to continue while the repository is in
473 # better than allowing a client to continue while the repository is in
474 # an inconsistent state.
474 # an inconsistent state.
475 ui.status(
475 ui.status(
476 _(
476 _(
477 b'marking source repository as being upgraded; clients will be '
477 b'marking source repository as being upgraded; clients will be '
478 b'unable to read from repository\n'
478 b'unable to read from repository\n'
479 )
479 )
480 )
480 )
481 scmutil.writereporequirements(
481 scmutil.writereporequirements(
482 srcrepo, srcrepo.requirements | {b'upgradeinprogress'}
482 srcrepo, srcrepo.requirements | {b'upgradeinprogress'}
483 )
483 )
484
484
485 ui.status(_(b'starting in-place swap of repository data\n'))
485 ui.status(_(b'starting in-place swap of repository data\n'))
486 ui.status(_(b'replaced files will be backed up at %s\n') % backuppath)
486 ui.status(_(b'replaced files will be backed up at %s\n') % backuppath)
487
487
488 # Now swap in the new store directory. Doing it as a rename should make
488 # Now swap in the new store directory. Doing it as a rename should make
489 # the operation nearly instantaneous and atomic (at least in well-behaved
489 # the operation nearly instantaneous and atomic (at least in well-behaved
490 # environments).
490 # environments).
491 ui.status(_(b'replacing store...\n'))
491 ui.status(_(b'replacing store...\n'))
492 tstart = util.timer()
492 tstart = util.timer()
493 util.rename(srcrepo.spath, backupvfs.join(b'store'))
493 util.rename(srcrepo.spath, backupvfs.join(b'store'))
494 util.rename(dstrepo.spath, srcrepo.spath)
494 util.rename(dstrepo.spath, srcrepo.spath)
495 elapsed = util.timer() - tstart
495 elapsed = util.timer() - tstart
496 ui.status(
496 ui.status(
497 _(
497 _(
498 b'store replacement complete; repository was inconsistent for '
498 b'store replacement complete; repository was inconsistent for '
499 b'%0.1fs\n'
499 b'%0.1fs\n'
500 )
500 )
501 % elapsed
501 % elapsed
502 )
502 )
503
503
504 # We first write the requirements file. Any new requirements will lock
504 # We first write the requirements file. Any new requirements will lock
505 # out legacy clients.
505 # out legacy clients.
506 ui.status(
506 ui.status(
507 _(
507 _(
508 b'finalizing requirements file and making repository readable '
508 b'finalizing requirements file and making repository readable '
509 b'again\n'
509 b'again\n'
510 )
510 )
511 )
511 )
512 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
512 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
513
513
514 # The lock file from the old store won't be removed because nothing has a
514 # The lock file from the old store won't be removed because nothing has a
515 # reference to its new location. So clean it up manually. Alternatively, we
515 # reference to its new location. So clean it up manually. Alternatively, we
516 # could update srcrepo.svfs and other variables to point to the new
516 # could update srcrepo.svfs and other variables to point to the new
517 # location. This is simpler.
517 # location. This is simpler.
518 backupvfs.unlink(b'store/lock')
518 backupvfs.unlink(b'store/lock')
519
519
520 return backuppath
520 return backuppath
General Comments 0
You need to be logged in to leave comments. Login now