##// END OF EJS Templates
upgrade: do not hardcore file extension of revlogs...
marmoute -
r47659:1c52d77d default
parent child Browse files
Show More
@@ -1,593 +1,593
1 1 # upgrade.py - functions for in place upgrade of Mercurial repository
2 2 #
3 3 # Copyright (c) 2016-present, Gregory Szorc
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 stat
11 11
12 12 from ..i18n import _
13 13 from ..pycompat import getattr
14 14 from .. import (
15 15 changelog,
16 16 error,
17 17 filelog,
18 18 manifest,
19 19 metadata,
20 20 pycompat,
21 21 requirements,
22 22 revlog,
23 23 scmutil,
24 24 store,
25 25 util,
26 26 vfs as vfsmod,
27 27 )
28 28 from ..revlogutils import nodemap
29 29
30 30
31 31 def _revlogfrompath(repo, rl_type, path):
32 32 """Obtain a revlog from a repo path.
33 33
34 34 An instance of the appropriate class is returned.
35 35 """
36 36 if rl_type & store.FILEFLAGS_CHANGELOG:
37 37 return changelog.changelog(repo.svfs)
38 38 elif rl_type & store.FILEFLAGS_MANIFESTLOG:
39 39 mandir = b''
40 40 if b'/' in path:
41 41 mandir = path.rsplit(b'/', 1)[0]
42 42 return manifest.manifestrevlog(
43 43 repo.nodeconstants, repo.svfs, tree=mandir
44 44 )
45 45 else:
46 46 # drop the extension and the `data/` prefix
47 47 path = path.rsplit(b'.', 1)[0].split(b'/', 1)[1]
48 48 return filelog.filelog(repo.svfs, path)
49 49
50 50
51 51 def _copyrevlog(tr, destrepo, oldrl, rl_type, unencodedname):
52 52 """copy all relevant files for `oldrl` into `destrepo` store
53 53
54 54 Files are copied "as is" without any transformation. The copy is performed
55 55 without extra checks. Callers are responsible for making sure the copied
56 56 content is compatible with format of the destination repository.
57 57 """
58 58 oldrl = getattr(oldrl, '_revlog', oldrl)
59 59 newrl = _revlogfrompath(destrepo, rl_type, unencodedname)
60 60 newrl = getattr(newrl, '_revlog', newrl)
61 61
62 62 oldvfs = oldrl.opener
63 63 newvfs = newrl.opener
64 64 oldindex = oldvfs.join(oldrl.indexfile)
65 65 newindex = newvfs.join(newrl.indexfile)
66 66 olddata = oldvfs.join(oldrl.datafile)
67 67 newdata = newvfs.join(newrl.datafile)
68 68
69 69 with newvfs(newrl.indexfile, b'w'):
70 70 pass # create all the directories
71 71
72 72 util.copyfile(oldindex, newindex)
73 73 copydata = oldrl.opener.exists(oldrl.datafile)
74 74 if copydata:
75 75 util.copyfile(olddata, newdata)
76 76
77 77 if rl_type & store.FILEFLAGS_FILELOG:
78 78 destrepo.svfs.fncache.add(unencodedname)
79 79 if copydata:
80 80 destrepo.svfs.fncache.add(unencodedname[:-2] + b'.d')
81 81
82 82
83 83 UPGRADE_CHANGELOG = b"changelog"
84 84 UPGRADE_MANIFEST = b"manifest"
85 85 UPGRADE_FILELOGS = b"all-filelogs"
86 86
87 87 UPGRADE_ALL_REVLOGS = frozenset(
88 88 [UPGRADE_CHANGELOG, UPGRADE_MANIFEST, UPGRADE_FILELOGS]
89 89 )
90 90
91 91
92 92 def getsidedatacompanion(srcrepo, dstrepo):
93 93 sidedatacompanion = None
94 94 removedreqs = srcrepo.requirements - dstrepo.requirements
95 95 addedreqs = dstrepo.requirements - srcrepo.requirements
96 96 if requirements.SIDEDATA_REQUIREMENT in removedreqs:
97 97
98 98 def sidedatacompanion(rl, rev):
99 99 rl = getattr(rl, '_revlog', rl)
100 100 if rl.flags(rev) & revlog.REVIDX_SIDEDATA:
101 101 return True, (), {}, 0, 0
102 102 return False, (), {}, 0, 0
103 103
104 104 elif requirements.COPIESSDC_REQUIREMENT in addedreqs:
105 105 sidedatacompanion = metadata.getsidedataadder(srcrepo, dstrepo)
106 106 elif requirements.COPIESSDC_REQUIREMENT in removedreqs:
107 107 sidedatacompanion = metadata.getsidedataremover(srcrepo, dstrepo)
108 108 return sidedatacompanion
109 109
110 110
111 111 def matchrevlog(revlogfilter, rl_type):
112 112 """check if a revlog is selected for cloning.
113 113
114 114 In other words, are there any updates which need to be done on revlog
115 115 or it can be blindly copied.
116 116
117 117 The store entry is checked against the passed filter"""
118 118 if rl_type & store.FILEFLAGS_CHANGELOG:
119 119 return UPGRADE_CHANGELOG in revlogfilter
120 120 elif rl_type & store.FILEFLAGS_MANIFESTLOG:
121 121 return UPGRADE_MANIFEST in revlogfilter
122 122 assert rl_type & store.FILEFLAGS_FILELOG
123 123 return UPGRADE_FILELOGS in revlogfilter
124 124
125 125
126 126 def _perform_clone(
127 127 ui,
128 128 dstrepo,
129 129 tr,
130 130 old_revlog,
131 131 rl_type,
132 132 unencoded,
133 133 upgrade_op,
134 134 sidedatacompanion,
135 135 oncopiedrevision,
136 136 ):
137 137 """ returns the new revlog object created"""
138 138 newrl = None
139 139 if matchrevlog(upgrade_op.revlogs_to_process, rl_type):
140 140 ui.note(
141 141 _(b'cloning %d revisions from %s\n') % (len(old_revlog), unencoded)
142 142 )
143 143 newrl = _revlogfrompath(dstrepo, rl_type, unencoded)
144 144 old_revlog.clone(
145 145 tr,
146 146 newrl,
147 147 addrevisioncb=oncopiedrevision,
148 148 deltareuse=upgrade_op.delta_reuse_mode,
149 149 forcedeltabothparents=upgrade_op.force_re_delta_both_parents,
150 150 sidedatacompanion=sidedatacompanion,
151 151 )
152 152 else:
153 153 msg = _(b'blindly copying %s containing %i revisions\n')
154 154 ui.note(msg % (unencoded, len(old_revlog)))
155 155 _copyrevlog(tr, dstrepo, old_revlog, rl_type, unencoded)
156 156
157 157 newrl = _revlogfrompath(dstrepo, rl_type, unencoded)
158 158 return newrl
159 159
160 160
161 161 def _clonerevlogs(
162 162 ui,
163 163 srcrepo,
164 164 dstrepo,
165 165 tr,
166 166 upgrade_op,
167 167 ):
168 168 """Copy revlogs between 2 repos."""
169 169 revcount = 0
170 170 srcsize = 0
171 171 srcrawsize = 0
172 172 dstsize = 0
173 173 fcount = 0
174 174 frevcount = 0
175 175 fsrcsize = 0
176 176 frawsize = 0
177 177 fdstsize = 0
178 178 mcount = 0
179 179 mrevcount = 0
180 180 msrcsize = 0
181 181 mrawsize = 0
182 182 mdstsize = 0
183 183 crevcount = 0
184 184 csrcsize = 0
185 185 crawsize = 0
186 186 cdstsize = 0
187 187
188 188 alldatafiles = list(srcrepo.store.walk())
189 189 # mapping of data files which needs to be cloned
190 190 # key is unencoded filename
191 191 # value is revlog_object_from_srcrepo
192 192 manifests = {}
193 193 changelogs = {}
194 194 filelogs = {}
195 195
196 196 # Perform a pass to collect metadata. This validates we can open all
197 197 # source files and allows a unified progress bar to be displayed.
198 198 for rl_type, unencoded, encoded, size in alldatafiles:
199 199 if not rl_type & store.FILEFLAGS_REVLOG_MAIN:
200 200 continue
201 201
202 202 rl = _revlogfrompath(srcrepo, rl_type, unencoded)
203 203
204 204 info = rl.storageinfo(
205 205 exclusivefiles=True,
206 206 revisionscount=True,
207 207 trackedsize=True,
208 208 storedsize=True,
209 209 )
210 210
211 211 revcount += info[b'revisionscount'] or 0
212 212 datasize = info[b'storedsize'] or 0
213 213 rawsize = info[b'trackedsize'] or 0
214 214
215 215 srcsize += datasize
216 216 srcrawsize += rawsize
217 217
218 218 # This is for the separate progress bars.
219 219 if rl_type & store.FILEFLAGS_CHANGELOG:
220 220 changelogs[unencoded] = (rl_type, rl)
221 221 crevcount += len(rl)
222 222 csrcsize += datasize
223 223 crawsize += rawsize
224 224 elif rl_type & store.FILEFLAGS_MANIFESTLOG:
225 225 manifests[unencoded] = (rl_type, rl)
226 226 mcount += 1
227 227 mrevcount += len(rl)
228 228 msrcsize += datasize
229 229 mrawsize += rawsize
230 230 elif rl_type & store.FILEFLAGS_FILELOG:
231 231 filelogs[unencoded] = (rl_type, rl)
232 232 fcount += 1
233 233 frevcount += len(rl)
234 234 fsrcsize += datasize
235 235 frawsize += rawsize
236 236 else:
237 237 error.ProgrammingError(b'unknown revlog type')
238 238
239 239 if not revcount:
240 240 return
241 241
242 242 ui.status(
243 243 _(
244 244 b'migrating %d total revisions (%d in filelogs, %d in manifests, '
245 245 b'%d in changelog)\n'
246 246 )
247 247 % (revcount, frevcount, mrevcount, crevcount)
248 248 )
249 249 ui.status(
250 250 _(b'migrating %s in store; %s tracked data\n')
251 251 % ((util.bytecount(srcsize), util.bytecount(srcrawsize)))
252 252 )
253 253
254 254 # Used to keep track of progress.
255 255 progress = None
256 256
257 257 def oncopiedrevision(rl, rev, node):
258 258 progress.increment()
259 259
260 260 sidedatacompanion = getsidedatacompanion(srcrepo, dstrepo)
261 261
262 262 # Migrating filelogs
263 263 ui.status(
264 264 _(
265 265 b'migrating %d filelogs containing %d revisions '
266 266 b'(%s in store; %s tracked data)\n'
267 267 )
268 268 % (
269 269 fcount,
270 270 frevcount,
271 271 util.bytecount(fsrcsize),
272 272 util.bytecount(frawsize),
273 273 )
274 274 )
275 275 progress = srcrepo.ui.makeprogress(_(b'file revisions'), total=frevcount)
276 276 for unencoded, (rl_type, oldrl) in sorted(filelogs.items()):
277 277 newrl = _perform_clone(
278 278 ui,
279 279 dstrepo,
280 280 tr,
281 281 oldrl,
282 282 rl_type,
283 283 unencoded,
284 284 upgrade_op,
285 285 sidedatacompanion,
286 286 oncopiedrevision,
287 287 )
288 288 info = newrl.storageinfo(storedsize=True)
289 289 fdstsize += info[b'storedsize'] or 0
290 290 ui.status(
291 291 _(
292 292 b'finished migrating %d filelog revisions across %d '
293 293 b'filelogs; change in size: %s\n'
294 294 )
295 295 % (frevcount, fcount, util.bytecount(fdstsize - fsrcsize))
296 296 )
297 297
298 298 # Migrating manifests
299 299 ui.status(
300 300 _(
301 301 b'migrating %d manifests containing %d revisions '
302 302 b'(%s in store; %s tracked data)\n'
303 303 )
304 304 % (
305 305 mcount,
306 306 mrevcount,
307 307 util.bytecount(msrcsize),
308 308 util.bytecount(mrawsize),
309 309 )
310 310 )
311 311 if progress:
312 312 progress.complete()
313 313 progress = srcrepo.ui.makeprogress(
314 314 _(b'manifest revisions'), total=mrevcount
315 315 )
316 316 for unencoded, (rl_type, oldrl) in sorted(manifests.items()):
317 317 newrl = _perform_clone(
318 318 ui,
319 319 dstrepo,
320 320 tr,
321 321 oldrl,
322 322 rl_type,
323 323 unencoded,
324 324 upgrade_op,
325 325 sidedatacompanion,
326 326 oncopiedrevision,
327 327 )
328 328 info = newrl.storageinfo(storedsize=True)
329 329 mdstsize += info[b'storedsize'] or 0
330 330 ui.status(
331 331 _(
332 332 b'finished migrating %d manifest revisions across %d '
333 333 b'manifests; change in size: %s\n'
334 334 )
335 335 % (mrevcount, mcount, util.bytecount(mdstsize - msrcsize))
336 336 )
337 337
338 338 # Migrating changelog
339 339 ui.status(
340 340 _(
341 341 b'migrating changelog containing %d revisions '
342 342 b'(%s in store; %s tracked data)\n'
343 343 )
344 344 % (
345 345 crevcount,
346 346 util.bytecount(csrcsize),
347 347 util.bytecount(crawsize),
348 348 )
349 349 )
350 350 if progress:
351 351 progress.complete()
352 352 progress = srcrepo.ui.makeprogress(
353 353 _(b'changelog revisions'), total=crevcount
354 354 )
355 355 for unencoded, (rl_type, oldrl) in sorted(changelogs.items()):
356 356 newrl = _perform_clone(
357 357 ui,
358 358 dstrepo,
359 359 tr,
360 360 oldrl,
361 361 rl_type,
362 362 unencoded,
363 363 upgrade_op,
364 364 sidedatacompanion,
365 365 oncopiedrevision,
366 366 )
367 367 info = newrl.storageinfo(storedsize=True)
368 368 cdstsize += info[b'storedsize'] or 0
369 369 progress.complete()
370 370 ui.status(
371 371 _(
372 372 b'finished migrating %d changelog revisions; change in size: '
373 373 b'%s\n'
374 374 )
375 375 % (crevcount, util.bytecount(cdstsize - csrcsize))
376 376 )
377 377
378 378 dstsize = fdstsize + mdstsize + cdstsize
379 379 ui.status(
380 380 _(
381 381 b'finished migrating %d total revisions; total change in store '
382 382 b'size: %s\n'
383 383 )
384 384 % (revcount, util.bytecount(dstsize - srcsize))
385 385 )
386 386
387 387
388 388 def _files_to_copy_post_revlog_clone(srcrepo):
389 389 """yields files which should be copied to destination after revlogs
390 390 are cloned"""
391 391 for path, kind, st in sorted(srcrepo.store.vfs.readdir(b'', stat=True)):
392 392 # don't copy revlogs as they are already cloned
393 if path.endswith((b'.i', b'.d', b'.n', b'.nd')):
393 if store.revlog_type(path) is not None:
394 394 continue
395 395 # Skip transaction related files.
396 396 if path.startswith(b'undo'):
397 397 continue
398 398 # Only copy regular files.
399 399 if kind != stat.S_IFREG:
400 400 continue
401 401 # Skip other skipped files.
402 402 if path in (b'lock', b'fncache'):
403 403 continue
404 404 # TODO: should we skip cache too?
405 405
406 406 yield path
407 407
408 408
409 409 def _replacestores(currentrepo, upgradedrepo, backupvfs, upgrade_op):
410 410 """Replace the stores after current repository is upgraded
411 411
412 412 Creates a backup of current repository store at backup path
413 413 Replaces upgraded store files in current repo from upgraded one
414 414
415 415 Arguments:
416 416 currentrepo: repo object of current repository
417 417 upgradedrepo: repo object of the upgraded data
418 418 backupvfs: vfs object for the backup path
419 419 upgrade_op: upgrade operation object
420 420 to be used to decide what all is upgraded
421 421 """
422 422 # TODO: don't blindly rename everything in store
423 423 # There can be upgrades where store is not touched at all
424 424 if upgrade_op.backup_store:
425 425 util.rename(currentrepo.spath, backupvfs.join(b'store'))
426 426 else:
427 427 currentrepo.vfs.rmtree(b'store', forcibly=True)
428 428 util.rename(upgradedrepo.spath, currentrepo.spath)
429 429
430 430
431 431 def finishdatamigration(ui, srcrepo, dstrepo, requirements):
432 432 """Hook point for extensions to perform additional actions during upgrade.
433 433
434 434 This function is called after revlogs and store files have been copied but
435 435 before the new store is swapped into the original location.
436 436 """
437 437
438 438
439 439 def upgrade(ui, srcrepo, dstrepo, upgrade_op):
440 440 """Do the low-level work of upgrading a repository.
441 441
442 442 The upgrade is effectively performed as a copy between a source
443 443 repository and a temporary destination repository.
444 444
445 445 The source repository is unmodified for as long as possible so the
446 446 upgrade can abort at any time without causing loss of service for
447 447 readers and without corrupting the source repository.
448 448 """
449 449 assert srcrepo.currentwlock()
450 450 assert dstrepo.currentwlock()
451 451 backuppath = None
452 452 backupvfs = None
453 453
454 454 ui.status(
455 455 _(
456 456 b'(it is safe to interrupt this process any time before '
457 457 b'data migration completes)\n'
458 458 )
459 459 )
460 460
461 461 if upgrade_op.requirements_only:
462 462 ui.status(_(b'upgrading repository requirements\n'))
463 463 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
464 464 # if there is only one action and that is persistent nodemap upgrade
465 465 # directly write the nodemap file and update requirements instead of going
466 466 # through the whole cloning process
467 467 elif (
468 468 len(upgrade_op.upgrade_actions) == 1
469 469 and b'persistent-nodemap' in upgrade_op._upgrade_actions_names
470 470 and not upgrade_op.removed_actions
471 471 ):
472 472 ui.status(
473 473 _(b'upgrading repository to use persistent nodemap feature\n')
474 474 )
475 475 with srcrepo.transaction(b'upgrade') as tr:
476 476 unfi = srcrepo.unfiltered()
477 477 cl = unfi.changelog
478 478 nodemap.persist_nodemap(tr, cl, force=True)
479 479 # we want to directly operate on the underlying revlog to force
480 480 # create a nodemap file. This is fine since this is upgrade code
481 481 # and it heavily relies on repository being revlog based
482 482 # hence accessing private attributes can be justified
483 483 nodemap.persist_nodemap(
484 484 tr, unfi.manifestlog._rootstore._revlog, force=True
485 485 )
486 486 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
487 487 elif (
488 488 len(upgrade_op.removed_actions) == 1
489 489 and [
490 490 x
491 491 for x in upgrade_op.removed_actions
492 492 if x.name == b'persistent-nodemap'
493 493 ]
494 494 and not upgrade_op.upgrade_actions
495 495 ):
496 496 ui.status(
497 497 _(b'downgrading repository to not use persistent nodemap feature\n')
498 498 )
499 499 with srcrepo.transaction(b'upgrade') as tr:
500 500 unfi = srcrepo.unfiltered()
501 501 cl = unfi.changelog
502 502 nodemap.delete_nodemap(tr, srcrepo, cl)
503 503 # check comment 20 lines above for accessing private attributes
504 504 nodemap.delete_nodemap(
505 505 tr, srcrepo, unfi.manifestlog._rootstore._revlog
506 506 )
507 507 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
508 508 else:
509 509 with dstrepo.transaction(b'upgrade') as tr:
510 510 _clonerevlogs(
511 511 ui,
512 512 srcrepo,
513 513 dstrepo,
514 514 tr,
515 515 upgrade_op,
516 516 )
517 517
518 518 # Now copy other files in the store directory.
519 519 for p in _files_to_copy_post_revlog_clone(srcrepo):
520 520 srcrepo.ui.status(_(b'copying %s\n') % p)
521 521 src = srcrepo.store.rawvfs.join(p)
522 522 dst = dstrepo.store.rawvfs.join(p)
523 523 util.copyfile(src, dst, copystat=True)
524 524
525 525 finishdatamigration(ui, srcrepo, dstrepo, requirements)
526 526
527 527 ui.status(_(b'data fully upgraded in a temporary repository\n'))
528 528
529 529 if upgrade_op.backup_store:
530 530 backuppath = pycompat.mkdtemp(
531 531 prefix=b'upgradebackup.', dir=srcrepo.path
532 532 )
533 533 backupvfs = vfsmod.vfs(backuppath)
534 534
535 535 # Make a backup of requires file first, as it is the first to be modified.
536 536 util.copyfile(
537 537 srcrepo.vfs.join(b'requires'), backupvfs.join(b'requires')
538 538 )
539 539
540 540 # We install an arbitrary requirement that clients must not support
541 541 # as a mechanism to lock out new clients during the data swap. This is
542 542 # better than allowing a client to continue while the repository is in
543 543 # an inconsistent state.
544 544 ui.status(
545 545 _(
546 546 b'marking source repository as being upgraded; clients will be '
547 547 b'unable to read from repository\n'
548 548 )
549 549 )
550 550 scmutil.writereporequirements(
551 551 srcrepo, srcrepo.requirements | {b'upgradeinprogress'}
552 552 )
553 553
554 554 ui.status(_(b'starting in-place swap of repository data\n'))
555 555 if upgrade_op.backup_store:
556 556 ui.status(
557 557 _(b'replaced files will be backed up at %s\n') % backuppath
558 558 )
559 559
560 560 # Now swap in the new store directory. Doing it as a rename should make
561 561 # the operation nearly instantaneous and atomic (at least in well-behaved
562 562 # environments).
563 563 ui.status(_(b'replacing store...\n'))
564 564 tstart = util.timer()
565 565 _replacestores(srcrepo, dstrepo, backupvfs, upgrade_op)
566 566 elapsed = util.timer() - tstart
567 567 ui.status(
568 568 _(
569 569 b'store replacement complete; repository was inconsistent for '
570 570 b'%0.1fs\n'
571 571 )
572 572 % elapsed
573 573 )
574 574
575 575 # We first write the requirements file. Any new requirements will lock
576 576 # out legacy clients.
577 577 ui.status(
578 578 _(
579 579 b'finalizing requirements file and making repository readable '
580 580 b'again\n'
581 581 )
582 582 )
583 583 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
584 584
585 585 if upgrade_op.backup_store:
586 586 # The lock file from the old store won't be removed because nothing has a
587 587 # reference to its new location. So clean it up manually. Alternatively, we
588 588 # could update srcrepo.svfs and other variables to point to the new
589 589 # location. This is simpler.
590 590 assert backupvfs is not None # help pytype
591 591 backupvfs.unlink(b'store/lock')
592 592
593 593 return backuppath
General Comments 0
You need to be logged in to leave comments. Login now