##// END OF EJS Templates
store: issue a single entry for each revlog...
marmoute -
r51389:e50d1fe7 default
parent child Browse files
Show More
@@ -1,576 +1,574
1 1 # repair.py - functions for repository repair for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 # Copyright 2007 Olivia Mackall
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9
10 10 from .i18n import _
11 11 from .node import (
12 12 hex,
13 13 short,
14 14 )
15 15 from . import (
16 16 bundle2,
17 17 changegroup,
18 18 discovery,
19 19 error,
20 20 exchange,
21 21 obsolete,
22 22 obsutil,
23 23 pathutil,
24 24 phases,
25 25 requirements,
26 26 scmutil,
27 27 store,
28 28 transaction,
29 29 util,
30 30 )
31 31 from .utils import (
32 32 hashutil,
33 33 urlutil,
34 34 )
35 35
36 36
37 37 def backupbundle(
38 38 repo,
39 39 bases,
40 40 heads,
41 41 node,
42 42 suffix,
43 43 compress=True,
44 44 obsolescence=True,
45 45 tmp_backup=False,
46 46 ):
47 47 """create a bundle with the specified revisions as a backup"""
48 48
49 49 backupdir = b"strip-backup"
50 50 vfs = repo.vfs
51 51 if not vfs.isdir(backupdir):
52 52 vfs.mkdir(backupdir)
53 53
54 54 # Include a hash of all the nodes in the filename for uniqueness
55 55 allcommits = repo.set(b'%ln::%ln', bases, heads)
56 56 allhashes = sorted(c.hex() for c in allcommits)
57 57 totalhash = hashutil.sha1(b''.join(allhashes)).digest()
58 58 name = b"%s/%s-%s-%s.hg" % (
59 59 backupdir,
60 60 short(node),
61 61 hex(totalhash[:4]),
62 62 suffix,
63 63 )
64 64
65 65 cgversion = changegroup.localversion(repo)
66 66 comp = None
67 67 if cgversion != b'01':
68 68 bundletype = b"HG20"
69 69 if compress:
70 70 comp = b'BZ'
71 71 elif compress:
72 72 bundletype = b"HG10BZ"
73 73 else:
74 74 bundletype = b"HG10UN"
75 75
76 76 outgoing = discovery.outgoing(repo, missingroots=bases, ancestorsof=heads)
77 77 contentopts = {
78 78 b'cg.version': cgversion,
79 79 b'obsolescence': obsolescence,
80 80 b'phases': True,
81 81 }
82 82 return bundle2.writenewbundle(
83 83 repo.ui,
84 84 repo,
85 85 b'strip',
86 86 name,
87 87 bundletype,
88 88 outgoing,
89 89 contentopts,
90 90 vfs,
91 91 compression=comp,
92 92 allow_internal=tmp_backup,
93 93 )
94 94
95 95
96 96 def _collectfiles(repo, striprev):
97 97 """find out the filelogs affected by the strip"""
98 98 files = set()
99 99
100 100 for x in range(striprev, len(repo)):
101 101 files.update(repo[x].files())
102 102
103 103 return sorted(files)
104 104
105 105
106 106 def _collectrevlog(revlog, striprev):
107 107 _, brokenset = revlog.getstrippoint(striprev)
108 108 return [revlog.linkrev(r) for r in brokenset]
109 109
110 110
111 111 def _collectbrokencsets(repo, files, striprev):
112 112 """return the changesets which will be broken by the truncation"""
113 113 s = set()
114 114
115 115 for revlog in manifestrevlogs(repo):
116 116 s.update(_collectrevlog(revlog, striprev))
117 117 for fname in files:
118 118 s.update(_collectrevlog(repo.file(fname), striprev))
119 119
120 120 return s
121 121
122 122
123 123 def strip(ui, repo, nodelist, backup=True, topic=b'backup'):
124 124 # This function requires the caller to lock the repo, but it operates
125 125 # within a transaction of its own, and thus requires there to be no current
126 126 # transaction when it is called.
127 127 if repo.currenttransaction() is not None:
128 128 raise error.ProgrammingError(b'cannot strip from inside a transaction')
129 129
130 130 # Simple way to maintain backwards compatibility for this
131 131 # argument.
132 132 if backup in [b'none', b'strip']:
133 133 backup = False
134 134
135 135 repo = repo.unfiltered()
136 136 repo.destroying()
137 137 vfs = repo.vfs
138 138 # load bookmark before changelog to avoid side effect from outdated
139 139 # changelog (see repo._refreshchangelog)
140 140 repo._bookmarks
141 141 cl = repo.changelog
142 142
143 143 # TODO handle undo of merge sets
144 144 if isinstance(nodelist, bytes):
145 145 nodelist = [nodelist]
146 146 striplist = [cl.rev(node) for node in nodelist]
147 147 striprev = min(striplist)
148 148
149 149 files = _collectfiles(repo, striprev)
150 150 saverevs = _collectbrokencsets(repo, files, striprev)
151 151
152 152 # Some revisions with rev > striprev may not be descendants of striprev.
153 153 # We have to find these revisions and put them in a bundle, so that
154 154 # we can restore them after the truncations.
155 155 # To create the bundle we use repo.changegroupsubset which requires
156 156 # the list of heads and bases of the set of interesting revisions.
157 157 # (head = revision in the set that has no descendant in the set;
158 158 # base = revision in the set that has no ancestor in the set)
159 159 tostrip = set(striplist)
160 160 saveheads = set(saverevs)
161 161 for r in cl.revs(start=striprev + 1):
162 162 if any(p in tostrip for p in cl.parentrevs(r)):
163 163 tostrip.add(r)
164 164
165 165 if r not in tostrip:
166 166 saverevs.add(r)
167 167 saveheads.difference_update(cl.parentrevs(r))
168 168 saveheads.add(r)
169 169 saveheads = [cl.node(r) for r in saveheads]
170 170
171 171 # compute base nodes
172 172 if saverevs:
173 173 descendants = set(cl.descendants(saverevs))
174 174 saverevs.difference_update(descendants)
175 175 savebases = [cl.node(r) for r in saverevs]
176 176 stripbases = [cl.node(r) for r in tostrip]
177 177
178 178 stripobsidx = obsmarkers = ()
179 179 if repo.ui.configbool(b'devel', b'strip-obsmarkers'):
180 180 obsmarkers = obsutil.exclusivemarkers(repo, stripbases)
181 181 if obsmarkers:
182 182 stripobsidx = [
183 183 i for i, m in enumerate(repo.obsstore) if m in obsmarkers
184 184 ]
185 185
186 186 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
187 187
188 188 backupfile = None
189 189 node = nodelist[-1]
190 190 if backup:
191 191 backupfile = _createstripbackup(repo, stripbases, node, topic)
192 192 # create a changegroup for all the branches we need to keep
193 193 tmpbundlefile = None
194 194 if saveheads:
195 195 # do not compress temporary bundle if we remove it from disk later
196 196 #
197 197 # We do not include obsolescence, it might re-introduce prune markers
198 198 # we are trying to strip. This is harmless since the stripped markers
199 199 # are already backed up and we did not touched the markers for the
200 200 # saved changesets.
201 201 tmpbundlefile = backupbundle(
202 202 repo,
203 203 savebases,
204 204 saveheads,
205 205 node,
206 206 b'temp',
207 207 compress=False,
208 208 obsolescence=False,
209 209 tmp_backup=True,
210 210 )
211 211
212 212 with ui.uninterruptible():
213 213 try:
214 214 with repo.transaction(b"strip") as tr:
215 215 # TODO this code violates the interface abstraction of the
216 216 # transaction and makes assumptions that file storage is
217 217 # using append-only files. We'll need some kind of storage
218 218 # API to handle stripping for us.
219 219 oldfiles = set(tr._offsetmap.keys())
220 220 oldfiles.update(tr._newfiles)
221 221
222 222 tr.startgroup()
223 223 cl.strip(striprev, tr)
224 224 stripmanifest(repo, striprev, tr, files)
225 225
226 226 for fn in files:
227 227 repo.file(fn).strip(striprev, tr)
228 228 tr.endgroup()
229 229
230 230 entries = tr.readjournal()
231 231
232 232 for file, troffset in entries:
233 233 if file in oldfiles:
234 234 continue
235 235 with repo.svfs(file, b'a', checkambig=True) as fp:
236 236 fp.truncate(troffset)
237 237 if troffset == 0:
238 238 repo.store.markremoved(file)
239 239
240 240 deleteobsmarkers(repo.obsstore, stripobsidx)
241 241 del repo.obsstore
242 242 repo.invalidatevolatilesets()
243 243 repo._phasecache.filterunknown(repo)
244 244
245 245 if tmpbundlefile:
246 246 ui.note(_(b"adding branch\n"))
247 247 f = vfs.open(tmpbundlefile, b"rb")
248 248 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
249 249 # silence internal shuffling chatter
250 250 maybe_silent = (
251 251 repo.ui.silent()
252 252 if not repo.ui.verbose
253 253 else util.nullcontextmanager()
254 254 )
255 255 with maybe_silent:
256 256 tmpbundleurl = b'bundle:' + vfs.join(tmpbundlefile)
257 257 txnname = b'strip'
258 258 if not isinstance(gen, bundle2.unbundle20):
259 259 txnname = b"strip\n%s" % urlutil.hidepassword(
260 260 tmpbundleurl
261 261 )
262 262 with repo.transaction(txnname) as tr:
263 263 bundle2.applybundle(
264 264 repo, gen, tr, source=b'strip', url=tmpbundleurl
265 265 )
266 266 f.close()
267 267
268 268 with repo.transaction(b'repair') as tr:
269 269 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
270 270 repo._bookmarks.applychanges(repo, tr, bmchanges)
271 271
272 272 transaction.cleanup_undo_files(repo.ui.warn, repo.vfs_map)
273 273
274 274 except: # re-raises
275 275 if backupfile:
276 276 ui.warn(
277 277 _(b"strip failed, backup bundle stored in '%s'\n")
278 278 % vfs.join(backupfile)
279 279 )
280 280 if tmpbundlefile:
281 281 ui.warn(
282 282 _(b"strip failed, unrecovered changes stored in '%s'\n")
283 283 % vfs.join(tmpbundlefile)
284 284 )
285 285 ui.warn(
286 286 _(
287 287 b"(fix the problem, then recover the changesets with "
288 288 b"\"hg unbundle '%s'\")\n"
289 289 )
290 290 % vfs.join(tmpbundlefile)
291 291 )
292 292 raise
293 293 else:
294 294 if tmpbundlefile:
295 295 # Remove temporary bundle only if there were no exceptions
296 296 vfs.unlink(tmpbundlefile)
297 297
298 298 repo.destroyed()
299 299 # return the backup file path (or None if 'backup' was False) so
300 300 # extensions can use it
301 301 return backupfile
302 302
303 303
304 304 def softstrip(ui, repo, nodelist, backup=True, topic=b'backup'):
305 305 """perform a "soft" strip using the archived phase"""
306 306 tostrip = [c.node() for c in repo.set(b'sort(%ln::)', nodelist)]
307 307 if not tostrip:
308 308 return None
309 309
310 310 backupfile = None
311 311 if backup:
312 312 node = tostrip[0]
313 313 backupfile = _createstripbackup(repo, tostrip, node, topic)
314 314
315 315 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
316 316 with repo.transaction(b'strip') as tr:
317 317 phases.retractboundary(repo, tr, phases.archived, tostrip)
318 318 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
319 319 repo._bookmarks.applychanges(repo, tr, bmchanges)
320 320 return backupfile
321 321
322 322
323 323 def _bookmarkmovements(repo, tostrip):
324 324 # compute necessary bookmark movement
325 325 bm = repo._bookmarks
326 326 updatebm = []
327 327 for m in bm:
328 328 rev = repo[bm[m]].rev()
329 329 if rev in tostrip:
330 330 updatebm.append(m)
331 331 newbmtarget = None
332 332 # If we need to move bookmarks, compute bookmark
333 333 # targets. Otherwise we can skip doing this logic.
334 334 if updatebm:
335 335 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)),
336 336 # but is much faster
337 337 newbmtarget = repo.revs(b'max(parents(%ld) - (%ld))', tostrip, tostrip)
338 338 if newbmtarget:
339 339 newbmtarget = repo[newbmtarget.first()].node()
340 340 else:
341 341 newbmtarget = b'.'
342 342 return newbmtarget, updatebm
343 343
344 344
345 345 def _createstripbackup(repo, stripbases, node, topic):
346 346 # backup the changeset we are about to strip
347 347 vfs = repo.vfs
348 348 unfi = repo.unfiltered()
349 349 to_node = unfi.changelog.node
350 350 # internal changeset are internal implementation details that should not
351 351 # leave the repository and not be exposed to the users. In addition feature
352 352 # using them requires to be resistant to strip. See test case for more
353 353 # details.
354 354 all_backup = unfi.revs(
355 355 b"(%ln)::(%ld) and not _internal()",
356 356 stripbases,
357 357 unfi.changelog.headrevs(),
358 358 )
359 359 if not all_backup:
360 360 return None
361 361
362 362 def to_nodes(revs):
363 363 return [to_node(r) for r in revs]
364 364
365 365 bases = to_nodes(unfi.revs("roots(%ld)", all_backup))
366 366 heads = to_nodes(unfi.revs("heads(%ld)", all_backup))
367 367 backupfile = backupbundle(repo, bases, heads, node, topic)
368 368 repo.ui.status(_(b"saved backup bundle to %s\n") % vfs.join(backupfile))
369 369 repo.ui.log(
370 370 b"backupbundle", b"saved backup bundle to %s\n", vfs.join(backupfile)
371 371 )
372 372 return backupfile
373 373
374 374
375 375 def safestriproots(ui, repo, nodes):
376 376 """return list of roots of nodes where descendants are covered by nodes"""
377 377 torev = repo.unfiltered().changelog.rev
378 378 revs = {torev(n) for n in nodes}
379 379 # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
380 380 # orphaned = affected - wanted
381 381 # affected = descendants(roots(wanted))
382 382 # wanted = revs
383 383 revset = b'%ld - ( ::( (roots(%ld):: and not _phase(%s)) -%ld) )'
384 384 tostrip = set(repo.revs(revset, revs, revs, phases.internal, revs))
385 385 notstrip = revs - tostrip
386 386 if notstrip:
387 387 nodestr = b', '.join(sorted(short(repo[n].node()) for n in notstrip))
388 388 ui.warn(
389 389 _(b'warning: orphaned descendants detected, not stripping %s\n')
390 390 % nodestr
391 391 )
392 392 return [c.node() for c in repo.set(b'roots(%ld)', tostrip)]
393 393
394 394
395 395 class stripcallback:
396 396 """used as a transaction postclose callback"""
397 397
398 398 def __init__(self, ui, repo, backup, topic):
399 399 self.ui = ui
400 400 self.repo = repo
401 401 self.backup = backup
402 402 self.topic = topic or b'backup'
403 403 self.nodelist = []
404 404
405 405 def addnodes(self, nodes):
406 406 self.nodelist.extend(nodes)
407 407
408 408 def __call__(self, tr):
409 409 roots = safestriproots(self.ui, self.repo, self.nodelist)
410 410 if roots:
411 411 strip(self.ui, self.repo, roots, self.backup, self.topic)
412 412
413 413
414 414 def delayedstrip(ui, repo, nodelist, topic=None, backup=True):
415 415 """like strip, but works inside transaction and won't strip irreverent revs
416 416
417 417 nodelist must explicitly contain all descendants. Otherwise a warning will
418 418 be printed that some nodes are not stripped.
419 419
420 420 Will do a backup if `backup` is True. The last non-None "topic" will be
421 421 used as the backup topic name. The default backup topic name is "backup".
422 422 """
423 423 tr = repo.currenttransaction()
424 424 if not tr:
425 425 nodes = safestriproots(ui, repo, nodelist)
426 426 return strip(ui, repo, nodes, backup=backup, topic=topic)
427 427 # transaction postclose callbacks are called in alphabet order.
428 428 # use '\xff' as prefix so we are likely to be called last.
429 429 callback = tr.getpostclose(b'\xffstrip')
430 430 if callback is None:
431 431 callback = stripcallback(ui, repo, backup=backup, topic=topic)
432 432 tr.addpostclose(b'\xffstrip', callback)
433 433 if topic:
434 434 callback.topic = topic
435 435 callback.addnodes(nodelist)
436 436
437 437
438 438 def stripmanifest(repo, striprev, tr, files):
439 439 for revlog in manifestrevlogs(repo):
440 440 revlog.strip(striprev, tr)
441 441
442 442
443 443 def manifestrevlogs(repo):
444 444 yield repo.manifestlog.getstorage(b'')
445 445 if scmutil.istreemanifest(repo):
446 446 # This logic is safe if treemanifest isn't enabled, but also
447 447 # pointless, so we skip it if treemanifest isn't enabled.
448 448 for entry in repo.store.datafiles():
449 449 if not entry.is_revlog:
450 450 continue
451 if not entry.revlog_type == store.FILEFLAGS_MANIFESTLOG:
452 continue
453 if entry.is_revlog_main:
451 if entry.revlog_type == store.FILEFLAGS_MANIFESTLOG:
454 452 yield repo.manifestlog.getstorage(entry.target_id)
455 453
456 454
457 455 def rebuildfncache(ui, repo, only_data=False):
458 456 """Rebuilds the fncache file from repo history.
459 457
460 458 Missing entries will be added. Extra entries will be removed.
461 459 """
462 460 repo = repo.unfiltered()
463 461
464 462 if requirements.FNCACHE_REQUIREMENT not in repo.requirements:
465 463 ui.warn(
466 464 _(
467 465 b'(not rebuilding fncache because repository does not '
468 466 b'support fncache)\n'
469 467 )
470 468 )
471 469 return
472 470
473 471 with repo.lock():
474 472 fnc = repo.store.fncache
475 473 fnc.ensureloaded(warn=ui.warn)
476 474
477 475 oldentries = set(fnc.entries)
478 476 newentries = set()
479 477 seenfiles = set()
480 478
481 479 if only_data:
482 480 # Trust the listing of .i from the fncache, but not the .d. This is
483 481 # much faster, because we only need to stat every possible .d files,
484 482 # instead of reading the full changelog
485 483 for f in fnc:
486 484 if f[:5] == b'data/' and f[-2:] == b'.i':
487 485 seenfiles.add(f[5:-2])
488 486 newentries.add(f)
489 487 dataf = f[:-2] + b'.d'
490 488 if repo.store._exists(dataf):
491 489 newentries.add(dataf)
492 490 else:
493 491 progress = ui.makeprogress(
494 492 _(b'rebuilding'), unit=_(b'changesets'), total=len(repo)
495 493 )
496 494 for rev in repo:
497 495 progress.update(rev)
498 496
499 497 ctx = repo[rev]
500 498 for f in ctx.files():
501 499 # This is to minimize I/O.
502 500 if f in seenfiles:
503 501 continue
504 502 seenfiles.add(f)
505 503
506 504 i = b'data/%s.i' % f
507 505 d = b'data/%s.d' % f
508 506
509 507 if repo.store._exists(i):
510 508 newentries.add(i)
511 509 if repo.store._exists(d):
512 510 newentries.add(d)
513 511
514 512 progress.complete()
515 513
516 514 if requirements.TREEMANIFEST_REQUIREMENT in repo.requirements:
517 515 # This logic is safe if treemanifest isn't enabled, but also
518 516 # pointless, so we skip it if treemanifest isn't enabled.
519 517 for dir in pathutil.dirs(seenfiles):
520 518 i = b'meta/%s/00manifest.i' % dir
521 519 d = b'meta/%s/00manifest.d' % dir
522 520
523 521 if repo.store._exists(i):
524 522 newentries.add(i)
525 523 if repo.store._exists(d):
526 524 newentries.add(d)
527 525
528 526 addcount = len(newentries - oldentries)
529 527 removecount = len(oldentries - newentries)
530 528 for p in sorted(oldentries - newentries):
531 529 ui.write(_(b'removing %s\n') % p)
532 530 for p in sorted(newentries - oldentries):
533 531 ui.write(_(b'adding %s\n') % p)
534 532
535 533 if addcount or removecount:
536 534 ui.write(
537 535 _(b'%d items added, %d removed from fncache\n')
538 536 % (addcount, removecount)
539 537 )
540 538 fnc.entries = newentries
541 539 fnc._dirty = True
542 540
543 541 with repo.transaction(b'fncache') as tr:
544 542 fnc.write(tr)
545 543 else:
546 544 ui.write(_(b'fncache already up to date\n'))
547 545
548 546
549 547 def deleteobsmarkers(obsstore, indices):
550 548 """Delete some obsmarkers from obsstore and return how many were deleted
551 549
552 550 'indices' is a list of ints which are the indices
553 551 of the markers to be deleted.
554 552
555 553 Every invocation of this function completely rewrites the obsstore file,
556 554 skipping the markers we want to be removed. The new temporary file is
557 555 created, remaining markers are written there and on .close() this file
558 556 gets atomically renamed to obsstore, thus guaranteeing consistency."""
559 557 if not indices:
560 558 # we don't want to rewrite the obsstore with the same content
561 559 return
562 560
563 561 left = []
564 562 current = obsstore._all
565 563 n = 0
566 564 for i, m in enumerate(current):
567 565 if i in indices:
568 566 n += 1
569 567 continue
570 568 left.append(m)
571 569
572 570 newobsstorefile = obsstore.svfs(b'obsstore', b'w', atomictemp=True)
573 571 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
574 572 newobsstorefile.write(bytes)
575 573 newobsstorefile.close()
576 574 return n
@@ -1,887 +1,885
1 1 # censor code related to censoring revision
2 2 # coding: utf8
3 3 #
4 4 # Copyright 2021 Pierre-Yves David <pierre-yves.david@octobus.net>
5 5 # Copyright 2015 Google, Inc <martinvonz@google.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 import binascii
11 11 import contextlib
12 12 import os
13 13 import struct
14 14
15 15 from ..node import (
16 16 nullrev,
17 17 )
18 18 from .constants import (
19 19 COMP_MODE_PLAIN,
20 20 ENTRY_DATA_COMPRESSED_LENGTH,
21 21 ENTRY_DATA_COMPRESSION_MODE,
22 22 ENTRY_DATA_OFFSET,
23 23 ENTRY_DATA_UNCOMPRESSED_LENGTH,
24 24 ENTRY_DELTA_BASE,
25 25 ENTRY_LINK_REV,
26 26 ENTRY_NODE_ID,
27 27 ENTRY_PARENT_1,
28 28 ENTRY_PARENT_2,
29 29 ENTRY_SIDEDATA_COMPRESSED_LENGTH,
30 30 ENTRY_SIDEDATA_COMPRESSION_MODE,
31 31 ENTRY_SIDEDATA_OFFSET,
32 32 REVIDX_ISCENSORED,
33 33 REVLOGV0,
34 34 REVLOGV1,
35 35 )
36 36 from ..i18n import _
37 37
38 38 from .. import (
39 39 error,
40 40 mdiff,
41 41 pycompat,
42 42 revlogutils,
43 43 util,
44 44 )
45 45 from ..utils import (
46 46 storageutil,
47 47 )
48 48 from . import (
49 49 constants,
50 50 deltas,
51 51 )
52 52
53 53
54 54 def v1_censor(rl, tr, censornode, tombstone=b''):
55 55 """censors a revision in a "version 1" revlog"""
56 56 assert rl._format_version == constants.REVLOGV1, rl._format_version
57 57
58 58 # avoid cycle
59 59 from .. import revlog
60 60
61 61 censorrev = rl.rev(censornode)
62 62 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
63 63
64 64 # Rewriting the revlog in place is hard. Our strategy for censoring is
65 65 # to create a new revlog, copy all revisions to it, then replace the
66 66 # revlogs on transaction close.
67 67 #
68 68 # This is a bit dangerous. We could easily have a mismatch of state.
69 69 newrl = revlog.revlog(
70 70 rl.opener,
71 71 target=rl.target,
72 72 radix=rl.radix,
73 73 postfix=b'tmpcensored',
74 74 censorable=True,
75 75 )
76 76 newrl._format_version = rl._format_version
77 77 newrl._format_flags = rl._format_flags
78 78 newrl._generaldelta = rl._generaldelta
79 79 newrl._parse_index = rl._parse_index
80 80
81 81 for rev in rl.revs():
82 82 node = rl.node(rev)
83 83 p1, p2 = rl.parents(node)
84 84
85 85 if rev == censorrev:
86 86 newrl.addrawrevision(
87 87 tombstone,
88 88 tr,
89 89 rl.linkrev(censorrev),
90 90 p1,
91 91 p2,
92 92 censornode,
93 93 constants.REVIDX_ISCENSORED,
94 94 )
95 95
96 96 if newrl.deltaparent(rev) != nullrev:
97 97 m = _(b'censored revision stored as delta; cannot censor')
98 98 h = _(
99 99 b'censoring of revlogs is not fully implemented;'
100 100 b' please report this bug'
101 101 )
102 102 raise error.Abort(m, hint=h)
103 103 continue
104 104
105 105 if rl.iscensored(rev):
106 106 if rl.deltaparent(rev) != nullrev:
107 107 m = _(
108 108 b'cannot censor due to censored '
109 109 b'revision having delta stored'
110 110 )
111 111 raise error.Abort(m)
112 112 rawtext = rl._chunk(rev)
113 113 else:
114 114 rawtext = rl.rawdata(rev)
115 115
116 116 newrl.addrawrevision(
117 117 rawtext, tr, rl.linkrev(rev), p1, p2, node, rl.flags(rev)
118 118 )
119 119
120 120 tr.addbackup(rl._indexfile, location=b'store')
121 121 if not rl._inline:
122 122 tr.addbackup(rl._datafile, location=b'store')
123 123
124 124 rl.opener.rename(newrl._indexfile, rl._indexfile)
125 125 if not rl._inline:
126 126 rl.opener.rename(newrl._datafile, rl._datafile)
127 127
128 128 rl.clearcaches()
129 129 rl._loadindex()
130 130
131 131
132 132 def v2_censor(revlog, tr, censornode, tombstone=b''):
133 133 """censors a revision in a "version 2" revlog"""
134 134 assert revlog._format_version != REVLOGV0, revlog._format_version
135 135 assert revlog._format_version != REVLOGV1, revlog._format_version
136 136
137 137 censor_revs = {revlog.rev(censornode)}
138 138 _rewrite_v2(revlog, tr, censor_revs, tombstone)
139 139
140 140
141 141 def _rewrite_v2(revlog, tr, censor_revs, tombstone=b''):
142 142 """rewrite a revlog to censor some of its content
143 143
144 144 General principle
145 145
146 146 We create new revlog files (index/data/sidedata) to copy the content of
147 147 the existing data without the censored data.
148 148
149 149 We need to recompute new delta for any revision that used the censored
150 150 revision as delta base. As the cumulative size of the new delta may be
151 151 large, we store them in a temporary file until they are stored in their
152 152 final destination.
153 153
154 154 All data before the censored data can be blindly copied. The rest needs
155 155 to be copied as we go and the associated index entry needs adjustement.
156 156 """
157 157 assert revlog._format_version != REVLOGV0, revlog._format_version
158 158 assert revlog._format_version != REVLOGV1, revlog._format_version
159 159
160 160 old_index = revlog.index
161 161 docket = revlog._docket
162 162
163 163 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
164 164
165 165 first_excl_rev = min(censor_revs)
166 166
167 167 first_excl_entry = revlog.index[first_excl_rev]
168 168 index_cutoff = revlog.index.entry_size * first_excl_rev
169 169 data_cutoff = first_excl_entry[ENTRY_DATA_OFFSET] >> 16
170 170 sidedata_cutoff = revlog.sidedata_cut_off(first_excl_rev)
171 171
172 172 with pycompat.unnamedtempfile(mode=b"w+b") as tmp_storage:
173 173 # rev β†’ (new_base, data_start, data_end, compression_mode)
174 174 rewritten_entries = _precompute_rewritten_delta(
175 175 revlog,
176 176 old_index,
177 177 censor_revs,
178 178 tmp_storage,
179 179 )
180 180
181 181 all_files = _setup_new_files(
182 182 revlog,
183 183 index_cutoff,
184 184 data_cutoff,
185 185 sidedata_cutoff,
186 186 )
187 187
188 188 # we dont need to open the old index file since its content already
189 189 # exist in a usable form in `old_index`.
190 190 with all_files() as open_files:
191 191 (
192 192 old_data_file,
193 193 old_sidedata_file,
194 194 new_index_file,
195 195 new_data_file,
196 196 new_sidedata_file,
197 197 ) = open_files
198 198
199 199 # writing the censored revision
200 200
201 201 # Writing all subsequent revisions
202 202 for rev in range(first_excl_rev, len(old_index)):
203 203 if rev in censor_revs:
204 204 _rewrite_censor(
205 205 revlog,
206 206 old_index,
207 207 open_files,
208 208 rev,
209 209 tombstone,
210 210 )
211 211 else:
212 212 _rewrite_simple(
213 213 revlog,
214 214 old_index,
215 215 open_files,
216 216 rev,
217 217 rewritten_entries,
218 218 tmp_storage,
219 219 )
220 220 docket.write(transaction=None, stripping=True)
221 221
222 222
223 223 def _precompute_rewritten_delta(
224 224 revlog,
225 225 old_index,
226 226 excluded_revs,
227 227 tmp_storage,
228 228 ):
229 229 """Compute new delta for revisions whose delta is based on revision that
230 230 will not survive as is.
231 231
232 232 Return a mapping: {rev β†’ (new_base, data_start, data_end, compression_mode)}
233 233 """
234 234 dc = deltas.deltacomputer(revlog)
235 235 rewritten_entries = {}
236 236 first_excl_rev = min(excluded_revs)
237 237 with revlog._segmentfile._open_read() as dfh:
238 238 for rev in range(first_excl_rev, len(old_index)):
239 239 if rev in excluded_revs:
240 240 # this revision will be preserved as is, so we don't need to
241 241 # consider recomputing a delta.
242 242 continue
243 243 entry = old_index[rev]
244 244 if entry[ENTRY_DELTA_BASE] not in excluded_revs:
245 245 continue
246 246 # This is a revision that use the censored revision as the base
247 247 # for its delta. We need a need new deltas
248 248 if entry[ENTRY_DATA_UNCOMPRESSED_LENGTH] == 0:
249 249 # this revision is empty, we can delta against nullrev
250 250 rewritten_entries[rev] = (nullrev, 0, 0, COMP_MODE_PLAIN)
251 251 else:
252 252
253 253 text = revlog.rawdata(rev, _df=dfh)
254 254 info = revlogutils.revisioninfo(
255 255 node=entry[ENTRY_NODE_ID],
256 256 p1=revlog.node(entry[ENTRY_PARENT_1]),
257 257 p2=revlog.node(entry[ENTRY_PARENT_2]),
258 258 btext=[text],
259 259 textlen=len(text),
260 260 cachedelta=None,
261 261 flags=entry[ENTRY_DATA_OFFSET] & 0xFFFF,
262 262 )
263 263 d = dc.finddeltainfo(
264 264 info, dfh, excluded_bases=excluded_revs, target_rev=rev
265 265 )
266 266 default_comp = revlog._docket.default_compression_header
267 267 comp_mode, d = deltas.delta_compression(default_comp, d)
268 268 # using `tell` is a bit lazy, but we are not here for speed
269 269 start = tmp_storage.tell()
270 270 tmp_storage.write(d.data[1])
271 271 end = tmp_storage.tell()
272 272 rewritten_entries[rev] = (d.base, start, end, comp_mode)
273 273 return rewritten_entries
274 274
275 275
276 276 def _setup_new_files(
277 277 revlog,
278 278 index_cutoff,
279 279 data_cutoff,
280 280 sidedata_cutoff,
281 281 ):
282 282 """
283 283
284 284 return a context manager to open all the relevant files:
285 285 - old_data_file,
286 286 - old_sidedata_file,
287 287 - new_index_file,
288 288 - new_data_file,
289 289 - new_sidedata_file,
290 290
291 291 The old_index_file is not here because it is accessed through the
292 292 `old_index` object if the caller function.
293 293 """
294 294 docket = revlog._docket
295 295 old_index_filepath = revlog.opener.join(docket.index_filepath())
296 296 old_data_filepath = revlog.opener.join(docket.data_filepath())
297 297 old_sidedata_filepath = revlog.opener.join(docket.sidedata_filepath())
298 298
299 299 new_index_filepath = revlog.opener.join(docket.new_index_file())
300 300 new_data_filepath = revlog.opener.join(docket.new_data_file())
301 301 new_sidedata_filepath = revlog.opener.join(docket.new_sidedata_file())
302 302
303 303 util.copyfile(old_index_filepath, new_index_filepath, nb_bytes=index_cutoff)
304 304 util.copyfile(old_data_filepath, new_data_filepath, nb_bytes=data_cutoff)
305 305 util.copyfile(
306 306 old_sidedata_filepath,
307 307 new_sidedata_filepath,
308 308 nb_bytes=sidedata_cutoff,
309 309 )
310 310 revlog.opener.register_file(docket.index_filepath())
311 311 revlog.opener.register_file(docket.data_filepath())
312 312 revlog.opener.register_file(docket.sidedata_filepath())
313 313
314 314 docket.index_end = index_cutoff
315 315 docket.data_end = data_cutoff
316 316 docket.sidedata_end = sidedata_cutoff
317 317
318 318 # reload the revlog internal information
319 319 revlog.clearcaches()
320 320 revlog._loadindex(docket=docket)
321 321
322 322 @contextlib.contextmanager
323 323 def all_files_opener():
324 324 # hide opening in an helper function to please check-code, black
325 325 # and various python version at the same time
326 326 with open(old_data_filepath, 'rb') as old_data_file:
327 327 with open(old_sidedata_filepath, 'rb') as old_sidedata_file:
328 328 with open(new_index_filepath, 'r+b') as new_index_file:
329 329 with open(new_data_filepath, 'r+b') as new_data_file:
330 330 with open(
331 331 new_sidedata_filepath, 'r+b'
332 332 ) as new_sidedata_file:
333 333 new_index_file.seek(0, os.SEEK_END)
334 334 assert new_index_file.tell() == index_cutoff
335 335 new_data_file.seek(0, os.SEEK_END)
336 336 assert new_data_file.tell() == data_cutoff
337 337 new_sidedata_file.seek(0, os.SEEK_END)
338 338 assert new_sidedata_file.tell() == sidedata_cutoff
339 339 yield (
340 340 old_data_file,
341 341 old_sidedata_file,
342 342 new_index_file,
343 343 new_data_file,
344 344 new_sidedata_file,
345 345 )
346 346
347 347 return all_files_opener
348 348
349 349
350 350 def _rewrite_simple(
351 351 revlog,
352 352 old_index,
353 353 all_files,
354 354 rev,
355 355 rewritten_entries,
356 356 tmp_storage,
357 357 ):
358 358 """append a normal revision to the index after the rewritten one(s)"""
359 359 (
360 360 old_data_file,
361 361 old_sidedata_file,
362 362 new_index_file,
363 363 new_data_file,
364 364 new_sidedata_file,
365 365 ) = all_files
366 366 entry = old_index[rev]
367 367 flags = entry[ENTRY_DATA_OFFSET] & 0xFFFF
368 368 old_data_offset = entry[ENTRY_DATA_OFFSET] >> 16
369 369
370 370 if rev not in rewritten_entries:
371 371 old_data_file.seek(old_data_offset)
372 372 new_data_size = entry[ENTRY_DATA_COMPRESSED_LENGTH]
373 373 new_data = old_data_file.read(new_data_size)
374 374 data_delta_base = entry[ENTRY_DELTA_BASE]
375 375 d_comp_mode = entry[ENTRY_DATA_COMPRESSION_MODE]
376 376 else:
377 377 (
378 378 data_delta_base,
379 379 start,
380 380 end,
381 381 d_comp_mode,
382 382 ) = rewritten_entries[rev]
383 383 new_data_size = end - start
384 384 tmp_storage.seek(start)
385 385 new_data = tmp_storage.read(new_data_size)
386 386
387 387 # It might be faster to group continuous read/write operation,
388 388 # however, this is censor, an operation that is not focussed
389 389 # around stellar performance. So I have not written this
390 390 # optimisation yet.
391 391 new_data_offset = new_data_file.tell()
392 392 new_data_file.write(new_data)
393 393
394 394 sidedata_size = entry[ENTRY_SIDEDATA_COMPRESSED_LENGTH]
395 395 new_sidedata_offset = new_sidedata_file.tell()
396 396 if 0 < sidedata_size:
397 397 old_sidedata_offset = entry[ENTRY_SIDEDATA_OFFSET]
398 398 old_sidedata_file.seek(old_sidedata_offset)
399 399 new_sidedata = old_sidedata_file.read(sidedata_size)
400 400 new_sidedata_file.write(new_sidedata)
401 401
402 402 data_uncompressed_length = entry[ENTRY_DATA_UNCOMPRESSED_LENGTH]
403 403 sd_com_mode = entry[ENTRY_SIDEDATA_COMPRESSION_MODE]
404 404 assert data_delta_base <= rev, (data_delta_base, rev)
405 405
406 406 new_entry = revlogutils.entry(
407 407 flags=flags,
408 408 data_offset=new_data_offset,
409 409 data_compressed_length=new_data_size,
410 410 data_uncompressed_length=data_uncompressed_length,
411 411 data_delta_base=data_delta_base,
412 412 link_rev=entry[ENTRY_LINK_REV],
413 413 parent_rev_1=entry[ENTRY_PARENT_1],
414 414 parent_rev_2=entry[ENTRY_PARENT_2],
415 415 node_id=entry[ENTRY_NODE_ID],
416 416 sidedata_offset=new_sidedata_offset,
417 417 sidedata_compressed_length=sidedata_size,
418 418 data_compression_mode=d_comp_mode,
419 419 sidedata_compression_mode=sd_com_mode,
420 420 )
421 421 revlog.index.append(new_entry)
422 422 entry_bin = revlog.index.entry_binary(rev)
423 423 new_index_file.write(entry_bin)
424 424
425 425 revlog._docket.index_end = new_index_file.tell()
426 426 revlog._docket.data_end = new_data_file.tell()
427 427 revlog._docket.sidedata_end = new_sidedata_file.tell()
428 428
429 429
430 430 def _rewrite_censor(
431 431 revlog,
432 432 old_index,
433 433 all_files,
434 434 rev,
435 435 tombstone,
436 436 ):
437 437 """rewrite and append a censored revision"""
438 438 (
439 439 old_data_file,
440 440 old_sidedata_file,
441 441 new_index_file,
442 442 new_data_file,
443 443 new_sidedata_file,
444 444 ) = all_files
445 445 entry = old_index[rev]
446 446
447 447 # XXX consider trying the default compression too
448 448 new_data_size = len(tombstone)
449 449 new_data_offset = new_data_file.tell()
450 450 new_data_file.write(tombstone)
451 451
452 452 # we are not adding any sidedata as they might leak info about the censored version
453 453
454 454 link_rev = entry[ENTRY_LINK_REV]
455 455
456 456 p1 = entry[ENTRY_PARENT_1]
457 457 p2 = entry[ENTRY_PARENT_2]
458 458
459 459 new_entry = revlogutils.entry(
460 460 flags=constants.REVIDX_ISCENSORED,
461 461 data_offset=new_data_offset,
462 462 data_compressed_length=new_data_size,
463 463 data_uncompressed_length=new_data_size,
464 464 data_delta_base=rev,
465 465 link_rev=link_rev,
466 466 parent_rev_1=p1,
467 467 parent_rev_2=p2,
468 468 node_id=entry[ENTRY_NODE_ID],
469 469 sidedata_offset=0,
470 470 sidedata_compressed_length=0,
471 471 data_compression_mode=COMP_MODE_PLAIN,
472 472 sidedata_compression_mode=COMP_MODE_PLAIN,
473 473 )
474 474 revlog.index.append(new_entry)
475 475 entry_bin = revlog.index.entry_binary(rev)
476 476 new_index_file.write(entry_bin)
477 477 revlog._docket.index_end = new_index_file.tell()
478 478 revlog._docket.data_end = new_data_file.tell()
479 479
480 480
481 481 def _get_filename_from_filelog_index(path):
482 482 # Drop the extension and the `data/` prefix
483 483 path_part = path.rsplit(b'.', 1)[0].split(b'/', 1)
484 484 if len(path_part) < 2:
485 485 msg = _(b"cannot recognize filelog from filename: '%s'")
486 486 msg %= path
487 487 raise error.Abort(msg)
488 488
489 489 return path_part[1]
490 490
491 491
492 492 def _filelog_from_filename(repo, path):
493 493 """Returns the filelog for the given `path`. Stolen from `engine.py`"""
494 494
495 495 from .. import filelog # avoid cycle
496 496
497 497 fl = filelog.filelog(repo.svfs, path)
498 498 return fl
499 499
500 500
501 501 def _write_swapped_parents(repo, rl, rev, offset, fp):
502 502 """Swaps p1 and p2 and overwrites the revlog entry for `rev` in `fp`"""
503 503 from ..pure import parsers # avoid cycle
504 504
505 505 if repo._currentlock(repo._lockref) is None:
506 506 # Let's be paranoid about it
507 507 msg = "repo needs to be locked to rewrite parents"
508 508 raise error.ProgrammingError(msg)
509 509
510 510 index_format = parsers.IndexObject.index_format
511 511 entry = rl.index[rev]
512 512 new_entry = list(entry)
513 513 new_entry[5], new_entry[6] = entry[6], entry[5]
514 514 packed = index_format.pack(*new_entry[:8])
515 515 fp.seek(offset)
516 516 fp.write(packed)
517 517
518 518
519 519 def _reorder_filelog_parents(repo, fl, to_fix):
520 520 """
521 521 Swaps p1 and p2 for all `to_fix` revisions of filelog `fl` and writes the
522 522 new version to disk, overwriting the old one with a rename.
523 523 """
524 524 from ..pure import parsers # avoid cycle
525 525
526 526 ui = repo.ui
527 527 assert len(to_fix) > 0
528 528 rl = fl._revlog
529 529 if rl._format_version != constants.REVLOGV1:
530 530 msg = "expected version 1 revlog, got version '%d'" % rl._format_version
531 531 raise error.ProgrammingError(msg)
532 532
533 533 index_file = rl._indexfile
534 534 new_file_path = index_file + b'.tmp-parents-fix'
535 535 repaired_msg = _(b"repaired revision %d of 'filelog %s'\n")
536 536
537 537 with ui.uninterruptible():
538 538 try:
539 539 util.copyfile(
540 540 rl.opener.join(index_file),
541 541 rl.opener.join(new_file_path),
542 542 checkambig=rl._checkambig,
543 543 )
544 544
545 545 with rl.opener(new_file_path, mode=b"r+") as fp:
546 546 if rl._inline:
547 547 index = parsers.InlinedIndexObject(fp.read())
548 548 for rev in fl.revs():
549 549 if rev in to_fix:
550 550 offset = index._calculate_index(rev)
551 551 _write_swapped_parents(repo, rl, rev, offset, fp)
552 552 ui.write(repaired_msg % (rev, index_file))
553 553 else:
554 554 index_format = parsers.IndexObject.index_format
555 555 for rev in to_fix:
556 556 offset = rev * index_format.size
557 557 _write_swapped_parents(repo, rl, rev, offset, fp)
558 558 ui.write(repaired_msg % (rev, index_file))
559 559
560 560 rl.opener.rename(new_file_path, index_file)
561 561 rl.clearcaches()
562 562 rl._loadindex()
563 563 finally:
564 564 util.tryunlink(new_file_path)
565 565
566 566
567 567 def _is_revision_affected(fl, filerev, metadata_cache=None):
568 568 full_text = lambda: fl._revlog.rawdata(filerev)
569 569 parent_revs = lambda: fl._revlog.parentrevs(filerev)
570 570 return _is_revision_affected_inner(
571 571 full_text, parent_revs, filerev, metadata_cache
572 572 )
573 573
574 574
575 575 def _is_revision_affected_inner(
576 576 full_text,
577 577 parents_revs,
578 578 filerev,
579 579 metadata_cache=None,
580 580 ):
581 581 """Mercurial currently (5.9rc0) uses `p1 == nullrev and p2 != nullrev` as a
582 582 special meaning compared to the reverse in the context of filelog-based
583 583 copytracing. issue6528 exists because new code assumed that parent ordering
584 584 didn't matter, so this detects if the revision contains metadata (since
585 585 it's only used for filelog-based copytracing) and its parents are in the
586 586 "wrong" order."""
587 587 try:
588 588 raw_text = full_text()
589 589 except error.CensoredNodeError:
590 590 # We don't care about censored nodes as they never carry metadata
591 591 return False
592 592
593 593 # raw text can be a `memoryview`, which doesn't implement `startswith`
594 594 has_meta = bytes(raw_text[:2]) == b'\x01\n'
595 595 if metadata_cache is not None:
596 596 metadata_cache[filerev] = has_meta
597 597 if has_meta:
598 598 (p1, p2) = parents_revs()
599 599 if p1 != nullrev and p2 == nullrev:
600 600 return True
601 601 return False
602 602
603 603
604 604 def _is_revision_affected_fast(repo, fl, filerev, metadata_cache):
605 605 rl = fl._revlog
606 606 is_censored = lambda: rl.iscensored(filerev)
607 607 delta_base = lambda: rl.deltaparent(filerev)
608 608 delta = lambda: rl._chunk(filerev)
609 609 full_text = lambda: rl.rawdata(filerev)
610 610 parent_revs = lambda: rl.parentrevs(filerev)
611 611 return _is_revision_affected_fast_inner(
612 612 is_censored,
613 613 delta_base,
614 614 delta,
615 615 full_text,
616 616 parent_revs,
617 617 filerev,
618 618 metadata_cache,
619 619 )
620 620
621 621
622 622 def _is_revision_affected_fast_inner(
623 623 is_censored,
624 624 delta_base,
625 625 delta,
626 626 full_text,
627 627 parent_revs,
628 628 filerev,
629 629 metadata_cache,
630 630 ):
631 631 """Optimization fast-path for `_is_revision_affected`.
632 632
633 633 `metadata_cache` is a dict of `{rev: has_metadata}` which allows any
634 634 revision to check if its base has metadata, saving computation of the full
635 635 text, instead looking at the current delta.
636 636
637 637 This optimization only works if the revisions are looked at in order."""
638 638
639 639 if is_censored():
640 640 # Censored revisions don't contain metadata, so they cannot be affected
641 641 metadata_cache[filerev] = False
642 642 return False
643 643
644 644 p1, p2 = parent_revs()
645 645 if p1 == nullrev or p2 != nullrev:
646 646 return False
647 647
648 648 delta_parent = delta_base()
649 649 parent_has_metadata = metadata_cache.get(delta_parent)
650 650 if parent_has_metadata is None:
651 651 return _is_revision_affected_inner(
652 652 full_text,
653 653 parent_revs,
654 654 filerev,
655 655 metadata_cache,
656 656 )
657 657
658 658 chunk = delta()
659 659 if not len(chunk):
660 660 # No diff for this revision
661 661 return parent_has_metadata
662 662
663 663 header_length = 12
664 664 if len(chunk) < header_length:
665 665 raise error.Abort(_(b"patch cannot be decoded"))
666 666
667 667 start, _end, _length = struct.unpack(b">lll", chunk[:header_length])
668 668
669 669 if start < 2: # len(b'\x01\n') == 2
670 670 # This delta does *something* to the metadata marker (if any).
671 671 # Check it the slow way
672 672 is_affected = _is_revision_affected_inner(
673 673 full_text,
674 674 parent_revs,
675 675 filerev,
676 676 metadata_cache,
677 677 )
678 678 return is_affected
679 679
680 680 # The diff did not remove or add the metadata header, it's then in the same
681 681 # situation as its parent
682 682 metadata_cache[filerev] = parent_has_metadata
683 683 return parent_has_metadata
684 684
685 685
686 686 def _from_report(ui, repo, context, from_report, dry_run):
687 687 """
688 688 Fix the revisions given in the `from_report` file, but still checks if the
689 689 revisions are indeed affected to prevent an unfortunate cyclic situation
690 690 where we'd swap well-ordered parents again.
691 691
692 692 See the doc for `debug_fix_issue6528` for the format documentation.
693 693 """
694 694 ui.write(_(b"loading report file '%s'\n") % from_report)
695 695
696 696 with context(), open(from_report, mode='rb') as f:
697 697 for line in f.read().split(b'\n'):
698 698 if not line:
699 699 continue
700 700 filenodes, filename = line.split(b' ', 1)
701 701 fl = _filelog_from_filename(repo, filename)
702 702 to_fix = set(
703 703 fl.rev(binascii.unhexlify(n)) for n in filenodes.split(b',')
704 704 )
705 705 excluded = set()
706 706
707 707 for filerev in to_fix:
708 708 if _is_revision_affected(fl, filerev):
709 709 msg = b"found affected revision %d for filelog '%s'\n"
710 710 ui.warn(msg % (filerev, filename))
711 711 else:
712 712 msg = _(b"revision %s of file '%s' is not affected\n")
713 713 msg %= (binascii.hexlify(fl.node(filerev)), filename)
714 714 ui.warn(msg)
715 715 excluded.add(filerev)
716 716
717 717 to_fix = to_fix - excluded
718 718 if not to_fix:
719 719 msg = _(b"no affected revisions were found for '%s'\n")
720 720 ui.write(msg % filename)
721 721 continue
722 722 if not dry_run:
723 723 _reorder_filelog_parents(repo, fl, sorted(to_fix))
724 724
725 725
726 726 def filter_delta_issue6528(revlog, deltas_iter):
727 727 """filter incomind deltas to repaire issue 6528 on the fly"""
728 728 metadata_cache = {}
729 729
730 730 deltacomputer = deltas.deltacomputer(revlog)
731 731
732 732 for rev, d in enumerate(deltas_iter, len(revlog)):
733 733 (
734 734 node,
735 735 p1_node,
736 736 p2_node,
737 737 linknode,
738 738 deltabase,
739 739 delta,
740 740 flags,
741 741 sidedata,
742 742 ) = d
743 743
744 744 if not revlog.index.has_node(deltabase):
745 745 raise error.LookupError(
746 746 deltabase, revlog.radix, _(b'unknown parent')
747 747 )
748 748 base_rev = revlog.rev(deltabase)
749 749 if not revlog.index.has_node(p1_node):
750 750 raise error.LookupError(p1_node, revlog.radix, _(b'unknown parent'))
751 751 p1_rev = revlog.rev(p1_node)
752 752 if not revlog.index.has_node(p2_node):
753 753 raise error.LookupError(p2_node, revlog.radix, _(b'unknown parent'))
754 754 p2_rev = revlog.rev(p2_node)
755 755
756 756 is_censored = lambda: bool(flags & REVIDX_ISCENSORED)
757 757 delta_base = lambda: revlog.rev(delta_base)
758 758 delta_base = lambda: base_rev
759 759 parent_revs = lambda: (p1_rev, p2_rev)
760 760
761 761 def full_text():
762 762 # note: being able to reuse the full text computation in the
763 763 # underlying addrevision would be useful however this is a bit too
764 764 # intrusive the for the "quick" issue6528 we are writing before the
765 765 # 5.8 release
766 766 textlen = mdiff.patchedsize(revlog.size(base_rev), delta)
767 767
768 768 revinfo = revlogutils.revisioninfo(
769 769 node,
770 770 p1_node,
771 771 p2_node,
772 772 [None],
773 773 textlen,
774 774 (base_rev, delta),
775 775 flags,
776 776 )
777 777 # cached by the global "writing" context
778 778 assert revlog._writinghandles is not None
779 779 if revlog._inline:
780 780 fh = revlog._writinghandles[0]
781 781 else:
782 782 fh = revlog._writinghandles[1]
783 783 return deltacomputer.buildtext(revinfo, fh)
784 784
785 785 is_affected = _is_revision_affected_fast_inner(
786 786 is_censored,
787 787 delta_base,
788 788 lambda: delta,
789 789 full_text,
790 790 parent_revs,
791 791 rev,
792 792 metadata_cache,
793 793 )
794 794 if is_affected:
795 795 d = (
796 796 node,
797 797 p2_node,
798 798 p1_node,
799 799 linknode,
800 800 deltabase,
801 801 delta,
802 802 flags,
803 803 sidedata,
804 804 )
805 805 yield d
806 806
807 807
808 808 def repair_issue6528(
809 809 ui, repo, dry_run=False, to_report=None, from_report=None, paranoid=False
810 810 ):
811 811 from .. import store # avoid cycle
812 812
813 813 @contextlib.contextmanager
814 814 def context():
815 815 if dry_run or to_report: # No need for locking
816 816 yield
817 817 else:
818 818 with repo.wlock(), repo.lock():
819 819 yield
820 820
821 821 if from_report:
822 822 return _from_report(ui, repo, context, from_report, dry_run)
823 823
824 824 report_entries = []
825 825
826 826 with context():
827 827 files = list(
828 828 entry
829 829 for entry in repo.store.datafiles()
830 830 if (
831 entry.is_revlog
832 and entry.is_revlog_main
833 and entry.revlog_type == store.FILEFLAGS_FILELOG
831 entry.is_revlog and entry.revlog_type == store.FILEFLAGS_FILELOG
834 832 )
835 833 )
836 834
837 835 progress = ui.makeprogress(
838 836 _(b"looking for affected revisions"),
839 837 unit=_(b"filelogs"),
840 838 total=len(files),
841 839 )
842 840 found_nothing = True
843 841
844 842 for entry in files:
845 843 progress.increment()
846 844 filename = entry.target_id
847 845 fl = _filelog_from_filename(repo, entry.target_id)
848 846
849 847 # Set of filerevs (or hex filenodes if `to_report`) that need fixing
850 848 to_fix = set()
851 849 metadata_cache = {}
852 850 for filerev in fl.revs():
853 851 affected = _is_revision_affected_fast(
854 852 repo, fl, filerev, metadata_cache
855 853 )
856 854 if paranoid:
857 855 slow = _is_revision_affected(fl, filerev)
858 856 if slow != affected:
859 857 msg = _(b"paranoid check failed for '%s' at node %s")
860 858 node = binascii.hexlify(fl.node(filerev))
861 859 raise error.Abort(msg % (filename, node))
862 860 if affected:
863 861 msg = b"found affected revision %d for file '%s'\n"
864 862 ui.warn(msg % (filerev, filename))
865 863 found_nothing = False
866 864 if not dry_run:
867 865 if to_report:
868 866 to_fix.add(binascii.hexlify(fl.node(filerev)))
869 867 else:
870 868 to_fix.add(filerev)
871 869
872 870 if to_fix:
873 871 to_fix = sorted(to_fix)
874 872 if to_report:
875 873 report_entries.append((filename, to_fix))
876 874 else:
877 875 _reorder_filelog_parents(repo, fl, to_fix)
878 876
879 877 if found_nothing:
880 878 ui.write(_(b"no affected revisions were found\n"))
881 879
882 880 if to_report and report_entries:
883 881 with open(to_report, mode="wb") as f:
884 882 for path, to_fix in report_entries:
885 883 f.write(b"%s %s\n" % (b",".join(to_fix), path))
886 884
887 885 progress.complete()
@@ -1,1056 +1,1067
1 1 # store.py - repository store handling for Mercurial
2 2 #
3 3 # Copyright 2008 Olivia Mackall <olivia@selenic.com>
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 import collections
9 9 import functools
10 10 import os
11 11 import re
12 12 import stat
13 13 from typing import Generator
14 14
15 15 from .i18n import _
16 16 from .pycompat import getattr
17 17 from .thirdparty import attr
18 18 from .node import hex
19 19 from . import (
20 20 changelog,
21 21 error,
22 22 manifest,
23 23 policy,
24 24 pycompat,
25 25 util,
26 26 vfs as vfsmod,
27 27 )
28 28 from .utils import hashutil
29 29
30 30 parsers = policy.importmod('parsers')
31 31 # how much bytes should be read from fncache in one read
32 32 # It is done to prevent loading large fncache files into memory
33 33 fncache_chunksize = 10 ** 6
34 34
35 35
36 36 def _match_tracked_entry(entry, matcher):
37 37 """parses a fncache entry and returns whether the entry is tracking a path
38 38 matched by matcher or not.
39 39
40 40 If matcher is None, returns True"""
41 41
42 42 if matcher is None:
43 43 return True
44 44 if entry.revlog_type == FILEFLAGS_FILELOG:
45 45 return matcher(entry.target_id)
46 46 elif entry.revlog_type == FILEFLAGS_MANIFESTLOG:
47 47 return matcher.visitdir(entry.target_id.rstrip(b'/'))
48 48 raise error.ProgrammingError(b"cannot process entry %r" % entry)
49 49
50 50
51 51 # This avoids a collision between a file named foo and a dir named
52 52 # foo.i or foo.d
53 53 def _encodedir(path):
54 54 """
55 55 >>> _encodedir(b'data/foo.i')
56 56 'data/foo.i'
57 57 >>> _encodedir(b'data/foo.i/bla.i')
58 58 'data/foo.i.hg/bla.i'
59 59 >>> _encodedir(b'data/foo.i.hg/bla.i')
60 60 'data/foo.i.hg.hg/bla.i'
61 61 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
62 62 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
63 63 """
64 64 return (
65 65 path.replace(b".hg/", b".hg.hg/")
66 66 .replace(b".i/", b".i.hg/")
67 67 .replace(b".d/", b".d.hg/")
68 68 )
69 69
70 70
71 71 encodedir = getattr(parsers, 'encodedir', _encodedir)
72 72
73 73
74 74 def decodedir(path):
75 75 """
76 76 >>> decodedir(b'data/foo.i')
77 77 'data/foo.i'
78 78 >>> decodedir(b'data/foo.i.hg/bla.i')
79 79 'data/foo.i/bla.i'
80 80 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
81 81 'data/foo.i.hg/bla.i'
82 82 """
83 83 if b".hg/" not in path:
84 84 return path
85 85 return (
86 86 path.replace(b".d.hg/", b".d/")
87 87 .replace(b".i.hg/", b".i/")
88 88 .replace(b".hg.hg/", b".hg/")
89 89 )
90 90
91 91
92 92 def _reserved():
93 93 """characters that are problematic for filesystems
94 94
95 95 * ascii escapes (0..31)
96 96 * ascii hi (126..255)
97 97 * windows specials
98 98
99 99 these characters will be escaped by encodefunctions
100 100 """
101 101 winreserved = [ord(x) for x in u'\\:*?"<>|']
102 102 for x in range(32):
103 103 yield x
104 104 for x in range(126, 256):
105 105 yield x
106 106 for x in winreserved:
107 107 yield x
108 108
109 109
110 110 def _buildencodefun():
111 111 """
112 112 >>> enc, dec = _buildencodefun()
113 113
114 114 >>> enc(b'nothing/special.txt')
115 115 'nothing/special.txt'
116 116 >>> dec(b'nothing/special.txt')
117 117 'nothing/special.txt'
118 118
119 119 >>> enc(b'HELLO')
120 120 '_h_e_l_l_o'
121 121 >>> dec(b'_h_e_l_l_o')
122 122 'HELLO'
123 123
124 124 >>> enc(b'hello:world?')
125 125 'hello~3aworld~3f'
126 126 >>> dec(b'hello~3aworld~3f')
127 127 'hello:world?'
128 128
129 129 >>> enc(b'the\\x07quick\\xADshot')
130 130 'the~07quick~adshot'
131 131 >>> dec(b'the~07quick~adshot')
132 132 'the\\x07quick\\xadshot'
133 133 """
134 134 e = b'_'
135 135 xchr = pycompat.bytechr
136 136 asciistr = list(map(xchr, range(127)))
137 137 capitals = list(range(ord(b"A"), ord(b"Z") + 1))
138 138
139 139 cmap = {x: x for x in asciistr}
140 140 for x in _reserved():
141 141 cmap[xchr(x)] = b"~%02x" % x
142 142 for x in capitals + [ord(e)]:
143 143 cmap[xchr(x)] = e + xchr(x).lower()
144 144
145 145 dmap = {}
146 146 for k, v in cmap.items():
147 147 dmap[v] = k
148 148
149 149 def decode(s):
150 150 i = 0
151 151 while i < len(s):
152 152 for l in range(1, 4):
153 153 try:
154 154 yield dmap[s[i : i + l]]
155 155 i += l
156 156 break
157 157 except KeyError:
158 158 pass
159 159 else:
160 160 raise KeyError
161 161
162 162 return (
163 163 lambda s: b''.join([cmap[s[c : c + 1]] for c in range(len(s))]),
164 164 lambda s: b''.join(list(decode(s))),
165 165 )
166 166
167 167
168 168 _encodefname, _decodefname = _buildencodefun()
169 169
170 170
171 171 def encodefilename(s):
172 172 """
173 173 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
174 174 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
175 175 """
176 176 return _encodefname(encodedir(s))
177 177
178 178
179 179 def decodefilename(s):
180 180 """
181 181 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
182 182 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
183 183 """
184 184 return decodedir(_decodefname(s))
185 185
186 186
187 187 def _buildlowerencodefun():
188 188 """
189 189 >>> f = _buildlowerencodefun()
190 190 >>> f(b'nothing/special.txt')
191 191 'nothing/special.txt'
192 192 >>> f(b'HELLO')
193 193 'hello'
194 194 >>> f(b'hello:world?')
195 195 'hello~3aworld~3f'
196 196 >>> f(b'the\\x07quick\\xADshot')
197 197 'the~07quick~adshot'
198 198 """
199 199 xchr = pycompat.bytechr
200 200 cmap = {xchr(x): xchr(x) for x in range(127)}
201 201 for x in _reserved():
202 202 cmap[xchr(x)] = b"~%02x" % x
203 203 for x in range(ord(b"A"), ord(b"Z") + 1):
204 204 cmap[xchr(x)] = xchr(x).lower()
205 205
206 206 def lowerencode(s):
207 207 return b"".join([cmap[c] for c in pycompat.iterbytestr(s)])
208 208
209 209 return lowerencode
210 210
211 211
212 212 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
213 213
214 214 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
215 215 _winres3 = (b'aux', b'con', b'prn', b'nul') # length 3
216 216 _winres4 = (b'com', b'lpt') # length 4 (with trailing 1..9)
217 217
218 218
219 219 def _auxencode(path, dotencode):
220 220 """
221 221 Encodes filenames containing names reserved by Windows or which end in
222 222 period or space. Does not touch other single reserved characters c.
223 223 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
224 224 Additionally encodes space or period at the beginning, if dotencode is
225 225 True. Parameter path is assumed to be all lowercase.
226 226 A segment only needs encoding if a reserved name appears as a
227 227 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
228 228 doesn't need encoding.
229 229
230 230 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
231 231 >>> _auxencode(s.split(b'/'), True)
232 232 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
233 233 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
234 234 >>> _auxencode(s.split(b'/'), False)
235 235 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
236 236 >>> _auxencode([b'foo. '], True)
237 237 ['foo.~20']
238 238 >>> _auxencode([b' .foo'], True)
239 239 ['~20.foo']
240 240 """
241 241 for i, n in enumerate(path):
242 242 if not n:
243 243 continue
244 244 if dotencode and n[0] in b'. ':
245 245 n = b"~%02x" % ord(n[0:1]) + n[1:]
246 246 path[i] = n
247 247 else:
248 248 l = n.find(b'.')
249 249 if l == -1:
250 250 l = len(n)
251 251 if (l == 3 and n[:3] in _winres3) or (
252 252 l == 4
253 253 and n[3:4] <= b'9'
254 254 and n[3:4] >= b'1'
255 255 and n[:3] in _winres4
256 256 ):
257 257 # encode third letter ('aux' -> 'au~78')
258 258 ec = b"~%02x" % ord(n[2:3])
259 259 n = n[0:2] + ec + n[3:]
260 260 path[i] = n
261 261 if n[-1] in b'. ':
262 262 # encode last period or space ('foo...' -> 'foo..~2e')
263 263 path[i] = n[:-1] + b"~%02x" % ord(n[-1:])
264 264 return path
265 265
266 266
267 267 _maxstorepathlen = 120
268 268 _dirprefixlen = 8
269 269 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
270 270
271 271
272 272 def _hashencode(path, dotencode):
273 273 digest = hex(hashutil.sha1(path).digest())
274 274 le = lowerencode(path[5:]).split(b'/') # skips prefix 'data/' or 'meta/'
275 275 parts = _auxencode(le, dotencode)
276 276 basename = parts[-1]
277 277 _root, ext = os.path.splitext(basename)
278 278 sdirs = []
279 279 sdirslen = 0
280 280 for p in parts[:-1]:
281 281 d = p[:_dirprefixlen]
282 282 if d[-1] in b'. ':
283 283 # Windows can't access dirs ending in period or space
284 284 d = d[:-1] + b'_'
285 285 if sdirslen == 0:
286 286 t = len(d)
287 287 else:
288 288 t = sdirslen + 1 + len(d)
289 289 if t > _maxshortdirslen:
290 290 break
291 291 sdirs.append(d)
292 292 sdirslen = t
293 293 dirs = b'/'.join(sdirs)
294 294 if len(dirs) > 0:
295 295 dirs += b'/'
296 296 res = b'dh/' + dirs + digest + ext
297 297 spaceleft = _maxstorepathlen - len(res)
298 298 if spaceleft > 0:
299 299 filler = basename[:spaceleft]
300 300 res = b'dh/' + dirs + filler + digest + ext
301 301 return res
302 302
303 303
304 304 def _hybridencode(path, dotencode):
305 305 """encodes path with a length limit
306 306
307 307 Encodes all paths that begin with 'data/', according to the following.
308 308
309 309 Default encoding (reversible):
310 310
311 311 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
312 312 characters are encoded as '~xx', where xx is the two digit hex code
313 313 of the character (see encodefilename).
314 314 Relevant path components consisting of Windows reserved filenames are
315 315 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
316 316
317 317 Hashed encoding (not reversible):
318 318
319 319 If the default-encoded path is longer than _maxstorepathlen, a
320 320 non-reversible hybrid hashing of the path is done instead.
321 321 This encoding uses up to _dirprefixlen characters of all directory
322 322 levels of the lowerencoded path, but not more levels than can fit into
323 323 _maxshortdirslen.
324 324 Then follows the filler followed by the sha digest of the full path.
325 325 The filler is the beginning of the basename of the lowerencoded path
326 326 (the basename is everything after the last path separator). The filler
327 327 is as long as possible, filling in characters from the basename until
328 328 the encoded path has _maxstorepathlen characters (or all chars of the
329 329 basename have been taken).
330 330 The extension (e.g. '.i' or '.d') is preserved.
331 331
332 332 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
333 333 encoding was used.
334 334 """
335 335 path = encodedir(path)
336 336 ef = _encodefname(path).split(b'/')
337 337 res = b'/'.join(_auxencode(ef, dotencode))
338 338 if len(res) > _maxstorepathlen:
339 339 res = _hashencode(path, dotencode)
340 340 return res
341 341
342 342
343 343 def _pathencode(path):
344 344 de = encodedir(path)
345 345 if len(path) > _maxstorepathlen:
346 346 return _hashencode(de, True)
347 347 ef = _encodefname(de).split(b'/')
348 348 res = b'/'.join(_auxencode(ef, True))
349 349 if len(res) > _maxstorepathlen:
350 350 return _hashencode(de, True)
351 351 return res
352 352
353 353
354 354 _pathencode = getattr(parsers, 'pathencode', _pathencode)
355 355
356 356
357 357 def _plainhybridencode(f):
358 358 return _hybridencode(f, False)
359 359
360 360
361 361 def _calcmode(vfs):
362 362 try:
363 363 # files in .hg/ will be created using this mode
364 364 mode = vfs.stat().st_mode
365 365 # avoid some useless chmods
366 366 if (0o777 & ~util.umask) == (0o777 & mode):
367 367 mode = None
368 368 except OSError:
369 369 mode = None
370 370 return mode
371 371
372 372
373 373 _data = [
374 374 b'bookmarks',
375 375 b'narrowspec',
376 376 b'data',
377 377 b'meta',
378 378 b'00manifest.d',
379 379 b'00manifest.i',
380 380 b'00changelog.d',
381 381 b'00changelog.i',
382 382 b'phaseroots',
383 383 b'obsstore',
384 384 b'requires',
385 385 ]
386 386
387 387 REVLOG_FILES_MAIN_EXT = (b'.i',)
388 388 REVLOG_FILES_OTHER_EXT = (
389 389 b'.idx',
390 390 b'.d',
391 391 b'.dat',
392 392 b'.n',
393 393 b'.nd',
394 394 b'.sda',
395 395 )
396 396 # file extension that also use a `-SOMELONGIDHASH.ext` form
397 397 REVLOG_FILES_LONG_EXT = (
398 398 b'.nd',
399 399 b'.idx',
400 400 b'.dat',
401 401 b'.sda',
402 402 )
403 403 # files that are "volatile" and might change between listing and streaming
404 404 #
405 405 # note: the ".nd" file are nodemap data and won't "change" but they might be
406 406 # deleted.
407 407 REVLOG_FILES_VOLATILE_EXT = (b'.n', b'.nd')
408 408
409 409 # some exception to the above matching
410 410 #
411 411 # XXX This is currently not in use because of issue6542
412 412 EXCLUDED = re.compile(br'.*undo\.[^/]+\.(nd?|i)$')
413 413
414 414
415 415 def is_revlog(f, kind, st):
416 416 if kind != stat.S_IFREG:
417 417 return None
418 418 return revlog_type(f)
419 419
420 420
421 421 def revlog_type(f):
422 422 # XXX we need to filter `undo.` created by the transaction here, however
423 423 # being naive about it also filter revlog for `undo.*` files, leading to
424 424 # issue6542. So we no longer use EXCLUDED.
425 425 if f.endswith(REVLOG_FILES_MAIN_EXT):
426 426 return FILEFLAGS_REVLOG_MAIN
427 427 elif f.endswith(REVLOG_FILES_OTHER_EXT):
428 428 t = FILETYPE_FILELOG_OTHER
429 429 if f.endswith(REVLOG_FILES_VOLATILE_EXT):
430 430 t |= FILEFLAGS_VOLATILE
431 431 return t
432 432 return None
433 433
434 434
435 435 # the file is part of changelog data
436 436 FILEFLAGS_CHANGELOG = 1 << 13
437 437 # the file is part of manifest data
438 438 FILEFLAGS_MANIFESTLOG = 1 << 12
439 439 # the file is part of filelog data
440 440 FILEFLAGS_FILELOG = 1 << 11
441 441 # file that are not directly part of a revlog
442 442 FILEFLAGS_OTHER = 1 << 10
443 443
444 444 # the main entry point for a revlog
445 445 FILEFLAGS_REVLOG_MAIN = 1 << 1
446 446 # a secondary file for a revlog
447 447 FILEFLAGS_REVLOG_OTHER = 1 << 0
448 448
449 449 # files that are "volatile" and might change between listing and streaming
450 450 FILEFLAGS_VOLATILE = 1 << 20
451 451
452 452 FILETYPE_CHANGELOG_MAIN = FILEFLAGS_CHANGELOG | FILEFLAGS_REVLOG_MAIN
453 453 FILETYPE_CHANGELOG_OTHER = FILEFLAGS_CHANGELOG | FILEFLAGS_REVLOG_OTHER
454 454 FILETYPE_MANIFESTLOG_MAIN = FILEFLAGS_MANIFESTLOG | FILEFLAGS_REVLOG_MAIN
455 455 FILETYPE_MANIFESTLOG_OTHER = FILEFLAGS_MANIFESTLOG | FILEFLAGS_REVLOG_OTHER
456 456 FILETYPE_FILELOG_MAIN = FILEFLAGS_FILELOG | FILEFLAGS_REVLOG_MAIN
457 457 FILETYPE_FILELOG_OTHER = FILEFLAGS_FILELOG | FILEFLAGS_REVLOG_OTHER
458 458 FILETYPE_OTHER = FILEFLAGS_OTHER
459 459
460 460
461 461 @attr.s(slots=True, init=False)
462 462 class BaseStoreEntry:
463 463 """An entry in the store
464 464
465 465 This is returned by `store.walk` and represent some data in the store."""
466 466
467
468 @attr.s(slots=True, init=False)
469 class SimpleStoreEntry(BaseStoreEntry):
470 """A generic entry in the store"""
471
472 is_revlog = False
473
467 474 _entry_path = attr.ib()
468 475 _is_volatile = attr.ib(default=False)
469 476 _file_size = attr.ib(default=None)
470 477
471 478 def __init__(
472 479 self,
473 480 entry_path,
474 481 is_volatile=False,
475 482 file_size=None,
476 483 ):
484 super().__init__()
477 485 self._entry_path = entry_path
478 486 self._is_volatile = is_volatile
479 487 self._file_size = file_size
480 488
481 489 def files(self):
482 490 return [
483 491 StoreFile(
484 492 unencoded_path=self._entry_path,
485 493 file_size=self._file_size,
486 494 is_volatile=self._is_volatile,
487 495 )
488 496 ]
489 497
490 498
491 499 @attr.s(slots=True, init=False)
492 class SimpleStoreEntry(BaseStoreEntry):
493 """A generic entry in the store"""
494
495 is_revlog = False
496
497
498 @attr.s(slots=True, init=False)
499 500 class RevlogStoreEntry(BaseStoreEntry):
500 501 """A revlog entry in the store"""
501 502
502 503 is_revlog = True
504
503 505 revlog_type = attr.ib(default=None)
504 506 target_id = attr.ib(default=None)
505 is_revlog_main = attr.ib(default=None)
507 _path_prefix = attr.ib(default=None)
508 _details = attr.ib(default=None)
506 509
507 510 def __init__(
508 511 self,
509 entry_path,
510 512 revlog_type,
513 path_prefix,
511 514 target_id,
512 is_revlog_main=False,
513 is_volatile=False,
514 file_size=None,
515 details,
515 516 ):
516 super().__init__(
517 entry_path=entry_path,
518 is_volatile=is_volatile,
519 file_size=file_size,
520 )
517 super().__init__()
521 518 self.revlog_type = revlog_type
522 519 self.target_id = target_id
523 self.is_revlog_main = is_revlog_main
520 self._path_prefix = path_prefix
521 assert b'.i' in details, (path_prefix, details)
522 self._details = details
524 523
525 524 def main_file_path(self):
526 525 """unencoded path of the main revlog file"""
527 return self._entry_path
526 return self._path_prefix + b'.i'
527
528 def files(self):
529 files = []
530 for ext in sorted(self._details, key=_ext_key):
531 path = self._path_prefix + ext
532 data = self._details[ext]
533 files.append(StoreFile(unencoded_path=path, **data))
534 return files
528 535
529 536
530 537 @attr.s(slots=True)
531 538 class StoreFile:
532 539 """a file matching an entry"""
533 540
534 541 unencoded_path = attr.ib()
535 _file_size = attr.ib(default=False)
542 _file_size = attr.ib(default=None)
536 543 is_volatile = attr.ib(default=False)
537 544
538 545 def file_size(self, vfs):
539 546 if self._file_size is not None:
540 547 return self._file_size
541 548 try:
542 549 return vfs.stat(self.unencoded_path).st_size
543 550 except FileNotFoundError:
544 551 return 0
545 552
546 553
547 554 def _gather_revlog(files_data):
548 555 """group files per revlog prefix
549 556
550 557 The returns a two level nested dict. The top level key is the revlog prefix
551 558 without extension, the second level is all the file "suffix" that were
552 559 seen for this revlog and arbitrary file data as value.
553 560 """
554 561 revlogs = collections.defaultdict(dict)
555 562 for u, value in files_data:
556 563 name, ext = _split_revlog_ext(u)
557 564 revlogs[name][ext] = value
558 565 return sorted(revlogs.items())
559 566
560 567
561 568 def _split_revlog_ext(filename):
562 569 """split the revlog file prefix from the variable extension"""
563 570 if filename.endswith(REVLOG_FILES_LONG_EXT):
564 571 char = b'-'
565 572 else:
566 573 char = b'.'
567 574 idx = filename.rfind(char)
568 575 return filename[:idx], filename[idx:]
569 576
570 577
571 578 def _ext_key(ext):
572 579 """a key to order revlog suffix
573 580
574 581 important to issue .i after other entry."""
575 582 # the only important part of this order is to keep the `.i` last.
576 583 if ext.endswith(b'.n'):
577 584 return (0, ext)
578 585 elif ext.endswith(b'.nd'):
579 586 return (10, ext)
580 587 elif ext.endswith(b'.d'):
581 588 return (20, ext)
582 589 elif ext.endswith(b'.i'):
583 590 return (50, ext)
584 591 else:
585 592 return (40, ext)
586 593
587 594
588 595 class basicstore:
589 596 '''base class for local repository stores'''
590 597
591 598 def __init__(self, path, vfstype):
592 599 vfs = vfstype(path)
593 600 self.path = vfs.base
594 601 self.createmode = _calcmode(vfs)
595 602 vfs.createmode = self.createmode
596 603 self.rawvfs = vfs
597 604 self.vfs = vfsmod.filtervfs(vfs, encodedir)
598 605 self.opener = self.vfs
599 606
600 607 def join(self, f):
601 608 return self.path + b'/' + encodedir(f)
602 609
603 610 def _walk(self, relpath, recurse, undecodable=None):
604 611 '''yields (revlog_type, unencoded, size)'''
605 612 path = self.path
606 613 if relpath:
607 614 path += b'/' + relpath
608 615 striplen = len(self.path) + 1
609 616 l = []
610 617 if self.rawvfs.isdir(path):
611 618 visit = [path]
612 619 readdir = self.rawvfs.readdir
613 620 while visit:
614 621 p = visit.pop()
615 622 for f, kind, st in readdir(p, stat=True):
616 623 fp = p + b'/' + f
617 624 rl_type = is_revlog(f, kind, st)
618 625 if rl_type is not None:
619 626 n = util.pconvert(fp[striplen:])
620 627 l.append((decodedir(n), (rl_type, st.st_size)))
621 628 elif kind == stat.S_IFDIR and recurse:
622 629 visit.append(fp)
623 630
624 631 l.sort()
625 632 return l
626 633
627 634 def changelog(self, trypending, concurrencychecker=None):
628 635 return changelog.changelog(
629 636 self.vfs,
630 637 trypending=trypending,
631 638 concurrencychecker=concurrencychecker,
632 639 )
633 640
634 641 def manifestlog(self, repo, storenarrowmatch):
635 642 rootstore = manifest.manifestrevlog(repo.nodeconstants, self.vfs)
636 643 return manifest.manifestlog(self.vfs, repo, rootstore, storenarrowmatch)
637 644
638 645 def datafiles(
639 646 self, matcher=None, undecodable=None
640 647 ) -> Generator[BaseStoreEntry, None, None]:
641 648 """Like walk, but excluding the changelog and root manifest.
642 649
643 650 When [undecodable] is None, revlogs names that can't be
644 651 decoded cause an exception. When it is provided, it should
645 652 be a list and the filenames that can't be decoded are added
646 653 to it instead. This is very rarely needed."""
647 654 dirs = [
648 655 (b'data', FILEFLAGS_FILELOG),
649 656 (b'meta', FILEFLAGS_MANIFESTLOG),
650 657 ]
651 658 for base_dir, rl_type in dirs:
652 659 files = self._walk(base_dir, True, undecodable=undecodable)
653 660 files = (f for f in files if f[1][0] is not None)
654 661 for revlog, details in _gather_revlog(files):
655 for ext, (t, s) in sorted(details.items()):
656 u = revlog + ext
662 file_details = {}
657 663 revlog_target_id = revlog.split(b'/', 1)[1]
664 for ext, (t, s) in sorted(details.items()):
665 file_details[ext] = {
666 'is_volatile': bool(t & FILEFLAGS_VOLATILE),
667 'file_size': s,
668 }
658 669 yield RevlogStoreEntry(
659 entry_path=u,
670 path_prefix=revlog,
660 671 revlog_type=rl_type,
661 672 target_id=revlog_target_id,
662 is_revlog_main=bool(t & FILEFLAGS_REVLOG_MAIN),
663 is_volatile=bool(t & FILEFLAGS_VOLATILE),
664 file_size=s,
673 details=file_details,
665 674 )
666 675
667 676 def topfiles(self) -> Generator[BaseStoreEntry, None, None]:
668 677 files = reversed(self._walk(b'', False))
669 678
670 679 changelogs = collections.defaultdict(dict)
671 680 manifestlogs = collections.defaultdict(dict)
672 681
673 682 for u, (t, s) in files:
674 683 if u.startswith(b'00changelog'):
675 684 name, ext = _split_revlog_ext(u)
676 685 changelogs[name][ext] = (t, s)
677 686 elif u.startswith(b'00manifest'):
678 687 name, ext = _split_revlog_ext(u)
679 688 manifestlogs[name][ext] = (t, s)
680 689 else:
681 690 yield SimpleStoreEntry(
682 691 entry_path=u,
683 692 is_volatile=bool(t & FILEFLAGS_VOLATILE),
684 693 file_size=s,
685 694 )
686 695 # yield manifest before changelog
687 696 top_rl = [
688 697 (manifestlogs, FILEFLAGS_MANIFESTLOG),
689 698 (changelogs, FILEFLAGS_CHANGELOG),
690 699 ]
691 700 assert len(manifestlogs) <= 1
692 701 assert len(changelogs) <= 1
693 702 for data, revlog_type in top_rl:
694 703 for revlog, details in sorted(data.items()):
695 # (keeping ordering so we get 00changelog.i last)
696 key = lambda x: _ext_key(x[0])
697 for ext, (t, s) in sorted(details.items(), key=key):
698 u = revlog + ext
704 file_details = {}
705 for ext, (t, s) in details.items():
706 file_details[ext] = {
707 'is_volatile': bool(t & FILEFLAGS_VOLATILE),
708 'file_size': s,
709 }
699 710 yield RevlogStoreEntry(
700 entry_path=u,
711 path_prefix=revlog,
701 712 revlog_type=revlog_type,
702 713 target_id=b'',
703 is_revlog_main=bool(t & FILEFLAGS_REVLOG_MAIN),
704 is_volatile=bool(t & FILEFLAGS_VOLATILE),
705 file_size=s,
714 details=file_details,
706 715 )
707 716
708 717 def walk(self, matcher=None) -> Generator[BaseStoreEntry, None, None]:
709 718 """return files related to data storage (ie: revlogs)
710 719
711 720 yields (file_type, unencoded, size)
712 721
713 722 if a matcher is passed, storage files of only those tracked paths
714 723 are passed with matches the matcher
715 724 """
716 725 # yield data files first
717 726 for x in self.datafiles(matcher):
718 727 yield x
719 728 for x in self.topfiles():
720 729 yield x
721 730
722 731 def copylist(self):
723 732 return _data
724 733
725 734 def write(self, tr):
726 735 pass
727 736
728 737 def invalidatecaches(self):
729 738 pass
730 739
731 740 def markremoved(self, fn):
732 741 pass
733 742
734 743 def __contains__(self, path):
735 744 '''Checks if the store contains path'''
736 745 path = b"/".join((b"data", path))
737 746 # file?
738 747 if self.vfs.exists(path + b".i"):
739 748 return True
740 749 # dir?
741 750 if not path.endswith(b"/"):
742 751 path = path + b"/"
743 752 return self.vfs.exists(path)
744 753
745 754
746 755 class encodedstore(basicstore):
747 756 def __init__(self, path, vfstype):
748 757 vfs = vfstype(path + b'/store')
749 758 self.path = vfs.base
750 759 self.createmode = _calcmode(vfs)
751 760 vfs.createmode = self.createmode
752 761 self.rawvfs = vfs
753 762 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
754 763 self.opener = self.vfs
755 764
756 765 def _walk(self, relpath, recurse, undecodable=None):
757 766 old = super()._walk(relpath, recurse)
758 767 new = []
759 768 for f1, value in old:
760 769 try:
761 770 f2 = decodefilename(f1)
762 771 except KeyError:
763 772 if undecodable is None:
764 773 msg = _(b'undecodable revlog name %s') % f1
765 774 raise error.StorageError(msg)
766 775 else:
767 776 undecodable.append(f1)
768 777 continue
769 778 new.append((f2, value))
770 779 return new
771 780
772 781 def datafiles(
773 782 self, matcher=None, undecodable=None
774 783 ) -> Generator[BaseStoreEntry, None, None]:
775 784 entries = super(encodedstore, self).datafiles(undecodable=undecodable)
776 785 for entry in entries:
777 786 if _match_tracked_entry(entry, matcher):
778 787 yield entry
779 788
780 789 def join(self, f):
781 790 return self.path + b'/' + encodefilename(f)
782 791
783 792 def copylist(self):
784 793 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in _data]
785 794
786 795
787 796 class fncache:
788 797 # the filename used to be partially encoded
789 798 # hence the encodedir/decodedir dance
790 799 def __init__(self, vfs):
791 800 self.vfs = vfs
792 801 self._ignores = set()
793 802 self.entries = None
794 803 self._dirty = False
795 804 # set of new additions to fncache
796 805 self.addls = set()
797 806
798 807 def ensureloaded(self, warn=None):
799 808 """read the fncache file if not already read.
800 809
801 810 If the file on disk is corrupted, raise. If warn is provided,
802 811 warn and keep going instead."""
803 812 if self.entries is None:
804 813 self._load(warn)
805 814
806 815 def _load(self, warn=None):
807 816 '''fill the entries from the fncache file'''
808 817 self._dirty = False
809 818 try:
810 819 fp = self.vfs(b'fncache', mode=b'rb')
811 820 except IOError:
812 821 # skip nonexistent file
813 822 self.entries = set()
814 823 return
815 824
816 825 self.entries = set()
817 826 chunk = b''
818 827 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
819 828 chunk += c
820 829 try:
821 830 p = chunk.rindex(b'\n')
822 831 self.entries.update(decodedir(chunk[: p + 1]).splitlines())
823 832 chunk = chunk[p + 1 :]
824 833 except ValueError:
825 834 # substring '\n' not found, maybe the entry is bigger than the
826 835 # chunksize, so let's keep iterating
827 836 pass
828 837
829 838 if chunk:
830 839 msg = _(b"fncache does not ends with a newline")
831 840 if warn:
832 841 warn(msg + b'\n')
833 842 else:
834 843 raise error.Abort(
835 844 msg,
836 845 hint=_(
837 846 b"use 'hg debugrebuildfncache' to "
838 847 b"rebuild the fncache"
839 848 ),
840 849 )
841 850 self._checkentries(fp, warn)
842 851 fp.close()
843 852
844 853 def _checkentries(self, fp, warn):
845 854 """make sure there is no empty string in entries"""
846 855 if b'' in self.entries:
847 856 fp.seek(0)
848 857 for n, line in enumerate(fp):
849 858 if not line.rstrip(b'\n'):
850 859 t = _(b'invalid entry in fncache, line %d') % (n + 1)
851 860 if warn:
852 861 warn(t + b'\n')
853 862 else:
854 863 raise error.Abort(t)
855 864
856 865 def write(self, tr):
857 866 if self._dirty:
858 867 assert self.entries is not None
859 868 self.entries = self.entries | self.addls
860 869 self.addls = set()
861 870 tr.addbackup(b'fncache')
862 871 fp = self.vfs(b'fncache', mode=b'wb', atomictemp=True)
863 872 if self.entries:
864 873 fp.write(encodedir(b'\n'.join(self.entries) + b'\n'))
865 874 fp.close()
866 875 self._dirty = False
867 876 if self.addls:
868 877 # if we have just new entries, let's append them to the fncache
869 878 tr.addbackup(b'fncache')
870 879 fp = self.vfs(b'fncache', mode=b'ab', atomictemp=True)
871 880 if self.addls:
872 881 fp.write(encodedir(b'\n'.join(self.addls) + b'\n'))
873 882 fp.close()
874 883 self.entries = None
875 884 self.addls = set()
876 885
877 886 def addignore(self, fn):
878 887 self._ignores.add(fn)
879 888
880 889 def add(self, fn):
881 890 if fn in self._ignores:
882 891 return
883 892 if self.entries is None:
884 893 self._load()
885 894 if fn not in self.entries:
886 895 self.addls.add(fn)
887 896
888 897 def remove(self, fn):
889 898 if self.entries is None:
890 899 self._load()
891 900 if fn in self.addls:
892 901 self.addls.remove(fn)
893 902 return
894 903 try:
895 904 self.entries.remove(fn)
896 905 self._dirty = True
897 906 except KeyError:
898 907 pass
899 908
900 909 def __contains__(self, fn):
901 910 if fn in self.addls:
902 911 return True
903 912 if self.entries is None:
904 913 self._load()
905 914 return fn in self.entries
906 915
907 916 def __iter__(self):
908 917 if self.entries is None:
909 918 self._load()
910 919 return iter(self.entries | self.addls)
911 920
912 921
913 922 class _fncachevfs(vfsmod.proxyvfs):
914 923 def __init__(self, vfs, fnc, encode):
915 924 vfsmod.proxyvfs.__init__(self, vfs)
916 925 self.fncache = fnc
917 926 self.encode = encode
918 927
919 928 def __call__(self, path, mode=b'r', *args, **kw):
920 929 encoded = self.encode(path)
921 930 if (
922 931 mode not in (b'r', b'rb')
923 932 and (path.startswith(b'data/') or path.startswith(b'meta/'))
924 933 and revlog_type(path) is not None
925 934 ):
926 935 # do not trigger a fncache load when adding a file that already is
927 936 # known to exist.
928 937 notload = self.fncache.entries is None and self.vfs.exists(encoded)
929 938 if notload and b'r+' in mode and not self.vfs.stat(encoded).st_size:
930 939 # when appending to an existing file, if the file has size zero,
931 940 # it should be considered as missing. Such zero-size files are
932 941 # the result of truncation when a transaction is aborted.
933 942 notload = False
934 943 if not notload:
935 944 self.fncache.add(path)
936 945 return self.vfs(encoded, mode, *args, **kw)
937 946
938 947 def join(self, path):
939 948 if path:
940 949 return self.vfs.join(self.encode(path))
941 950 else:
942 951 return self.vfs.join(path)
943 952
944 953 def register_file(self, path):
945 954 """generic hook point to lets fncache steer its stew"""
946 955 if path.startswith(b'data/') or path.startswith(b'meta/'):
947 956 self.fncache.add(path)
948 957
949 958
950 959 class fncachestore(basicstore):
951 960 def __init__(self, path, vfstype, dotencode):
952 961 if dotencode:
953 962 encode = _pathencode
954 963 else:
955 964 encode = _plainhybridencode
956 965 self.encode = encode
957 966 vfs = vfstype(path + b'/store')
958 967 self.path = vfs.base
959 968 self.pathsep = self.path + b'/'
960 969 self.createmode = _calcmode(vfs)
961 970 vfs.createmode = self.createmode
962 971 self.rawvfs = vfs
963 972 fnc = fncache(vfs)
964 973 self.fncache = fnc
965 974 self.vfs = _fncachevfs(vfs, fnc, encode)
966 975 self.opener = self.vfs
967 976
968 977 def join(self, f):
969 978 return self.pathsep + self.encode(f)
970 979
971 980 def getsize(self, path):
972 981 return self.rawvfs.stat(path).st_size
973 982
974 983 def datafiles(
975 984 self, matcher=None, undecodable=None
976 985 ) -> Generator[BaseStoreEntry, None, None]:
977 986 files = ((f, revlog_type(f)) for f in self.fncache)
978 987 # Note: all files in fncache should be revlog related, However the
979 988 # fncache might contains such file added by previous version of
980 989 # Mercurial.
981 990 files = (f for f in files if f[1] is not None)
982 991 by_revlog = _gather_revlog(files)
983 992 for revlog, details in by_revlog:
993 file_details = {}
984 994 if revlog.startswith(b'data/'):
985 995 rl_type = FILEFLAGS_FILELOG
986 996 revlog_target_id = revlog.split(b'/', 1)[1]
987 997 elif revlog.startswith(b'meta/'):
988 998 rl_type = FILEFLAGS_MANIFESTLOG
989 999 # drop the initial directory and the `00manifest` file part
990 1000 tmp = revlog.split(b'/', 1)[1]
991 1001 revlog_target_id = tmp.rsplit(b'/', 1)[0] + b'/'
992 1002 else:
993 1003 # unreachable
994 1004 assert False, revlog
995 for ext, t in sorted(details.items()):
996 f = revlog + ext
1005 for ext, t in details.items():
1006 file_details[ext] = {
1007 'is_volatile': bool(t & FILEFLAGS_VOLATILE),
1008 }
997 1009 entry = RevlogStoreEntry(
998 entry_path=f,
1010 path_prefix=revlog,
999 1011 revlog_type=rl_type,
1000 1012 target_id=revlog_target_id,
1001 is_revlog_main=bool(t & FILEFLAGS_REVLOG_MAIN),
1002 is_volatile=bool(t & FILEFLAGS_VOLATILE),
1013 details=file_details,
1003 1014 )
1004 1015 if _match_tracked_entry(entry, matcher):
1005 1016 yield entry
1006 1017
1007 1018 def copylist(self):
1008 1019 d = (
1009 1020 b'bookmarks',
1010 1021 b'narrowspec',
1011 1022 b'data',
1012 1023 b'meta',
1013 1024 b'dh',
1014 1025 b'fncache',
1015 1026 b'phaseroots',
1016 1027 b'obsstore',
1017 1028 b'00manifest.d',
1018 1029 b'00manifest.i',
1019 1030 b'00changelog.d',
1020 1031 b'00changelog.i',
1021 1032 b'requires',
1022 1033 )
1023 1034 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in d]
1024 1035
1025 1036 def write(self, tr):
1026 1037 self.fncache.write(tr)
1027 1038
1028 1039 def invalidatecaches(self):
1029 1040 self.fncache.entries = None
1030 1041 self.fncache.addls = set()
1031 1042
1032 1043 def markremoved(self, fn):
1033 1044 self.fncache.remove(fn)
1034 1045
1035 1046 def _exists(self, f):
1036 1047 ef = self.encode(f)
1037 1048 try:
1038 1049 self.getsize(ef)
1039 1050 return True
1040 1051 except FileNotFoundError:
1041 1052 return False
1042 1053
1043 1054 def __contains__(self, path):
1044 1055 '''Checks if the store contains path'''
1045 1056 path = b"/".join((b"data", path))
1046 1057 # check for files (exact match)
1047 1058 e = path + b'.i'
1048 1059 if e in self.fncache and self._exists(e):
1049 1060 return True
1050 1061 # now check for directories (prefix match)
1051 1062 if not path.endswith(b'/'):
1052 1063 path += b'/'
1053 1064 for e in self.fncache:
1054 1065 if e.startswith(path) and self._exists(e):
1055 1066 return True
1056 1067 return False
@@ -1,668 +1,668
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
9 9 import stat
10 10
11 11 from ..i18n import _
12 12 from ..pycompat import getattr
13 13 from .. import (
14 14 changelog,
15 15 error,
16 16 filelog,
17 17 manifest,
18 18 metadata,
19 19 pycompat,
20 20 requirements,
21 21 scmutil,
22 22 store,
23 23 util,
24 24 vfs as vfsmod,
25 25 )
26 26 from ..revlogutils import (
27 27 constants as revlogconst,
28 28 flagutil,
29 29 nodemap,
30 30 sidedata as sidedatamod,
31 31 )
32 32 from . import actions as upgrade_actions
33 33
34 34
35 35 def get_sidedata_helpers(srcrepo, dstrepo):
36 36 use_w = srcrepo.ui.configbool(b'experimental', b'worker.repository-upgrade')
37 37 sequential = pycompat.iswindows or not use_w
38 38 if not sequential:
39 39 srcrepo.register_sidedata_computer(
40 40 revlogconst.KIND_CHANGELOG,
41 41 sidedatamod.SD_FILES,
42 42 (sidedatamod.SD_FILES,),
43 43 metadata._get_worker_sidedata_adder(srcrepo, dstrepo),
44 44 flagutil.REVIDX_HASCOPIESINFO,
45 45 replace=True,
46 46 )
47 47 return sidedatamod.get_sidedata_helpers(srcrepo, dstrepo._wanted_sidedata)
48 48
49 49
50 50 def _revlog_from_store_entry(repo, entry):
51 51 """Obtain a revlog from a repo store entry.
52 52
53 53 An instance of the appropriate class is returned.
54 54 """
55 55 if entry.revlog_type == store.FILEFLAGS_CHANGELOG:
56 56 return changelog.changelog(repo.svfs)
57 57 elif entry.revlog_type == store.FILEFLAGS_MANIFESTLOG:
58 58 mandir = entry.target_id.rstrip(b'/')
59 59 return manifest.manifestrevlog(
60 60 repo.nodeconstants, repo.svfs, tree=mandir
61 61 )
62 62 else:
63 63 return filelog.filelog(repo.svfs, entry.target_id)
64 64
65 65
66 66 def _copyrevlog(tr, destrepo, oldrl, entry):
67 67 """copy all relevant files for `oldrl` into `destrepo` store
68 68
69 69 Files are copied "as is" without any transformation. The copy is performed
70 70 without extra checks. Callers are responsible for making sure the copied
71 71 content is compatible with format of the destination repository.
72 72 """
73 73 oldrl = getattr(oldrl, '_revlog', oldrl)
74 74 newrl = _revlog_from_store_entry(destrepo, entry)
75 75 newrl = getattr(newrl, '_revlog', newrl)
76 76
77 77 oldvfs = oldrl.opener
78 78 newvfs = newrl.opener
79 79 oldindex = oldvfs.join(oldrl._indexfile)
80 80 newindex = newvfs.join(newrl._indexfile)
81 81 olddata = oldvfs.join(oldrl._datafile)
82 82 newdata = newvfs.join(newrl._datafile)
83 83
84 84 with newvfs(newrl._indexfile, b'w'):
85 85 pass # create all the directories
86 86
87 87 util.copyfile(oldindex, newindex)
88 88 copydata = oldrl.opener.exists(oldrl._datafile)
89 89 if copydata:
90 90 util.copyfile(olddata, newdata)
91 91
92 92 if entry.revlog_type & store.FILEFLAGS_FILELOG:
93 93 unencodedname = entry.main_file_path()
94 94 destrepo.svfs.fncache.add(unencodedname)
95 95 if copydata:
96 96 destrepo.svfs.fncache.add(unencodedname[:-2] + b'.d')
97 97
98 98
99 99 UPGRADE_CHANGELOG = b"changelog"
100 100 UPGRADE_MANIFEST = b"manifest"
101 101 UPGRADE_FILELOGS = b"all-filelogs"
102 102
103 103 UPGRADE_ALL_REVLOGS = frozenset(
104 104 [UPGRADE_CHANGELOG, UPGRADE_MANIFEST, UPGRADE_FILELOGS]
105 105 )
106 106
107 107
108 108 def matchrevlog(revlogfilter, rl_type):
109 109 """check if a revlog is selected for cloning.
110 110
111 111 In other words, are there any updates which need to be done on revlog
112 112 or it can be blindly copied.
113 113
114 114 The store entry is checked against the passed filter"""
115 115 if rl_type & store.FILEFLAGS_CHANGELOG:
116 116 return UPGRADE_CHANGELOG in revlogfilter
117 117 elif rl_type & store.FILEFLAGS_MANIFESTLOG:
118 118 return UPGRADE_MANIFEST in revlogfilter
119 119 assert rl_type & store.FILEFLAGS_FILELOG
120 120 return UPGRADE_FILELOGS in revlogfilter
121 121
122 122
123 123 def _perform_clone(
124 124 ui,
125 125 dstrepo,
126 126 tr,
127 127 old_revlog,
128 128 entry,
129 129 upgrade_op,
130 130 sidedata_helpers,
131 131 oncopiedrevision,
132 132 ):
133 133 """returns the new revlog object created"""
134 134 newrl = None
135 135 revlog_path = entry.main_file_path()
136 136 if matchrevlog(upgrade_op.revlogs_to_process, entry.revlog_type):
137 137 ui.note(
138 138 _(b'cloning %d revisions from %s\n')
139 139 % (len(old_revlog), revlog_path)
140 140 )
141 141 newrl = _revlog_from_store_entry(dstrepo, entry)
142 142 old_revlog.clone(
143 143 tr,
144 144 newrl,
145 145 addrevisioncb=oncopiedrevision,
146 146 deltareuse=upgrade_op.delta_reuse_mode,
147 147 forcedeltabothparents=upgrade_op.force_re_delta_both_parents,
148 148 sidedata_helpers=sidedata_helpers,
149 149 )
150 150 else:
151 151 msg = _(b'blindly copying %s containing %i revisions\n')
152 152 ui.note(msg % (revlog_path, len(old_revlog)))
153 153 _copyrevlog(tr, dstrepo, old_revlog, entry)
154 154
155 155 newrl = _revlog_from_store_entry(dstrepo, entry)
156 156 return newrl
157 157
158 158
159 159 def _clonerevlogs(
160 160 ui,
161 161 srcrepo,
162 162 dstrepo,
163 163 tr,
164 164 upgrade_op,
165 165 ):
166 166 """Copy revlogs between 2 repos."""
167 167 revcount = 0
168 168 srcsize = 0
169 169 srcrawsize = 0
170 170 dstsize = 0
171 171 fcount = 0
172 172 frevcount = 0
173 173 fsrcsize = 0
174 174 frawsize = 0
175 175 fdstsize = 0
176 176 mcount = 0
177 177 mrevcount = 0
178 178 msrcsize = 0
179 179 mrawsize = 0
180 180 mdstsize = 0
181 181 crevcount = 0
182 182 csrcsize = 0
183 183 crawsize = 0
184 184 cdstsize = 0
185 185
186 186 alldatafiles = list(srcrepo.store.walk())
187 187 # mapping of data files which needs to be cloned
188 188 # key is unencoded filename
189 189 # value is revlog_object_from_srcrepo
190 190 manifests = {}
191 191 changelogs = {}
192 192 filelogs = {}
193 193
194 194 # Perform a pass to collect metadata. This validates we can open all
195 195 # source files and allows a unified progress bar to be displayed.
196 196 for entry in alldatafiles:
197 if not (entry.is_revlog and entry.is_revlog_main):
197 if not entry.is_revlog:
198 198 continue
199 199
200 200 rl = _revlog_from_store_entry(srcrepo, entry)
201 201
202 202 info = rl.storageinfo(
203 203 exclusivefiles=True,
204 204 revisionscount=True,
205 205 trackedsize=True,
206 206 storedsize=True,
207 207 )
208 208
209 209 revcount += info[b'revisionscount'] or 0
210 210 datasize = info[b'storedsize'] or 0
211 211 rawsize = info[b'trackedsize'] or 0
212 212
213 213 srcsize += datasize
214 214 srcrawsize += rawsize
215 215
216 216 # This is for the separate progress bars.
217 217 if entry.revlog_type & store.FILEFLAGS_CHANGELOG:
218 218 changelogs[entry.target_id] = entry
219 219 crevcount += len(rl)
220 220 csrcsize += datasize
221 221 crawsize += rawsize
222 222 elif entry.revlog_type & store.FILEFLAGS_MANIFESTLOG:
223 223 manifests[entry.target_id] = entry
224 224 mcount += 1
225 225 mrevcount += len(rl)
226 226 msrcsize += datasize
227 227 mrawsize += rawsize
228 228 elif entry.revlog_type & store.FILEFLAGS_FILELOG:
229 229 filelogs[entry.target_id] = entry
230 230 fcount += 1
231 231 frevcount += len(rl)
232 232 fsrcsize += datasize
233 233 frawsize += rawsize
234 234 else:
235 235 error.ProgrammingError(b'unknown revlog type')
236 236
237 237 if not revcount:
238 238 return
239 239
240 240 ui.status(
241 241 _(
242 242 b'migrating %d total revisions (%d in filelogs, %d in manifests, '
243 243 b'%d in changelog)\n'
244 244 )
245 245 % (revcount, frevcount, mrevcount, crevcount)
246 246 )
247 247 ui.status(
248 248 _(b'migrating %s in store; %s tracked data\n')
249 249 % ((util.bytecount(srcsize), util.bytecount(srcrawsize)))
250 250 )
251 251
252 252 # Used to keep track of progress.
253 253 progress = None
254 254
255 255 def oncopiedrevision(rl, rev, node):
256 256 progress.increment()
257 257
258 258 sidedata_helpers = get_sidedata_helpers(srcrepo, dstrepo)
259 259
260 260 # Migrating filelogs
261 261 ui.status(
262 262 _(
263 263 b'migrating %d filelogs containing %d revisions '
264 264 b'(%s in store; %s tracked data)\n'
265 265 )
266 266 % (
267 267 fcount,
268 268 frevcount,
269 269 util.bytecount(fsrcsize),
270 270 util.bytecount(frawsize),
271 271 )
272 272 )
273 273 progress = srcrepo.ui.makeprogress(_(b'file revisions'), total=frevcount)
274 274 for target_id, entry in sorted(filelogs.items()):
275 275 oldrl = _revlog_from_store_entry(srcrepo, entry)
276 276
277 277 newrl = _perform_clone(
278 278 ui,
279 279 dstrepo,
280 280 tr,
281 281 oldrl,
282 282 entry,
283 283 upgrade_op,
284 284 sidedata_helpers,
285 285 oncopiedrevision,
286 286 )
287 287 info = newrl.storageinfo(storedsize=True)
288 288 fdstsize += info[b'storedsize'] or 0
289 289 ui.status(
290 290 _(
291 291 b'finished migrating %d filelog revisions across %d '
292 292 b'filelogs; change in size: %s\n'
293 293 )
294 294 % (frevcount, fcount, util.bytecount(fdstsize - fsrcsize))
295 295 )
296 296
297 297 # Migrating manifests
298 298 ui.status(
299 299 _(
300 300 b'migrating %d manifests containing %d revisions '
301 301 b'(%s in store; %s tracked data)\n'
302 302 )
303 303 % (
304 304 mcount,
305 305 mrevcount,
306 306 util.bytecount(msrcsize),
307 307 util.bytecount(mrawsize),
308 308 )
309 309 )
310 310 if progress:
311 311 progress.complete()
312 312 progress = srcrepo.ui.makeprogress(
313 313 _(b'manifest revisions'), total=mrevcount
314 314 )
315 315 for target_id, entry in sorted(manifests.items()):
316 316 oldrl = _revlog_from_store_entry(srcrepo, entry)
317 317 newrl = _perform_clone(
318 318 ui,
319 319 dstrepo,
320 320 tr,
321 321 oldrl,
322 322 entry,
323 323 upgrade_op,
324 324 sidedata_helpers,
325 325 oncopiedrevision,
326 326 )
327 327 info = newrl.storageinfo(storedsize=True)
328 328 mdstsize += info[b'storedsize'] or 0
329 329 ui.status(
330 330 _(
331 331 b'finished migrating %d manifest revisions across %d '
332 332 b'manifests; change in size: %s\n'
333 333 )
334 334 % (mrevcount, mcount, util.bytecount(mdstsize - msrcsize))
335 335 )
336 336
337 337 # Migrating changelog
338 338 ui.status(
339 339 _(
340 340 b'migrating changelog containing %d revisions '
341 341 b'(%s in store; %s tracked data)\n'
342 342 )
343 343 % (
344 344 crevcount,
345 345 util.bytecount(csrcsize),
346 346 util.bytecount(crawsize),
347 347 )
348 348 )
349 349 if progress:
350 350 progress.complete()
351 351 progress = srcrepo.ui.makeprogress(
352 352 _(b'changelog revisions'), total=crevcount
353 353 )
354 354 for target_id, entry in sorted(changelogs.items()):
355 355 oldrl = _revlog_from_store_entry(srcrepo, entry)
356 356 newrl = _perform_clone(
357 357 ui,
358 358 dstrepo,
359 359 tr,
360 360 oldrl,
361 361 entry,
362 362 upgrade_op,
363 363 sidedata_helpers,
364 364 oncopiedrevision,
365 365 )
366 366 info = newrl.storageinfo(storedsize=True)
367 367 cdstsize += info[b'storedsize'] or 0
368 368 progress.complete()
369 369 ui.status(
370 370 _(
371 371 b'finished migrating %d changelog revisions; change in size: '
372 372 b'%s\n'
373 373 )
374 374 % (crevcount, util.bytecount(cdstsize - csrcsize))
375 375 )
376 376
377 377 dstsize = fdstsize + mdstsize + cdstsize
378 378 ui.status(
379 379 _(
380 380 b'finished migrating %d total revisions; total change in store '
381 381 b'size: %s\n'
382 382 )
383 383 % (revcount, util.bytecount(dstsize - srcsize))
384 384 )
385 385
386 386
387 387 def _files_to_copy_post_revlog_clone(srcrepo):
388 388 """yields files which should be copied to destination after revlogs
389 389 are cloned"""
390 390 for path, kind, st in sorted(srcrepo.store.vfs.readdir(b'', stat=True)):
391 391 # don't copy revlogs as they are already cloned
392 392 if store.revlog_type(path) is not None:
393 393 continue
394 394 # Skip transaction related files.
395 395 if path.startswith(b'undo'):
396 396 continue
397 397 # Only copy regular files.
398 398 if kind != stat.S_IFREG:
399 399 continue
400 400 # Skip other skipped files.
401 401 if path in (b'lock', b'fncache'):
402 402 continue
403 403 # TODO: should we skip cache too?
404 404
405 405 yield path
406 406
407 407
408 408 def _replacestores(currentrepo, upgradedrepo, backupvfs, upgrade_op):
409 409 """Replace the stores after current repository is upgraded
410 410
411 411 Creates a backup of current repository store at backup path
412 412 Replaces upgraded store files in current repo from upgraded one
413 413
414 414 Arguments:
415 415 currentrepo: repo object of current repository
416 416 upgradedrepo: repo object of the upgraded data
417 417 backupvfs: vfs object for the backup path
418 418 upgrade_op: upgrade operation object
419 419 to be used to decide what all is upgraded
420 420 """
421 421 # TODO: don't blindly rename everything in store
422 422 # There can be upgrades where store is not touched at all
423 423 if upgrade_op.backup_store:
424 424 util.rename(currentrepo.spath, backupvfs.join(b'store'))
425 425 else:
426 426 currentrepo.vfs.rmtree(b'store', forcibly=True)
427 427 util.rename(upgradedrepo.spath, currentrepo.spath)
428 428
429 429
430 430 def finishdatamigration(ui, srcrepo, dstrepo, requirements):
431 431 """Hook point for extensions to perform additional actions during upgrade.
432 432
433 433 This function is called after revlogs and store files have been copied but
434 434 before the new store is swapped into the original location.
435 435 """
436 436
437 437
438 438 def upgrade(ui, srcrepo, dstrepo, upgrade_op):
439 439 """Do the low-level work of upgrading a repository.
440 440
441 441 The upgrade is effectively performed as a copy between a source
442 442 repository and a temporary destination repository.
443 443
444 444 The source repository is unmodified for as long as possible so the
445 445 upgrade can abort at any time without causing loss of service for
446 446 readers and without corrupting the source repository.
447 447 """
448 448 assert srcrepo.currentwlock()
449 449 assert dstrepo.currentwlock()
450 450 backuppath = None
451 451 backupvfs = None
452 452
453 453 ui.status(
454 454 _(
455 455 b'(it is safe to interrupt this process any time before '
456 456 b'data migration completes)\n'
457 457 )
458 458 )
459 459
460 460 if upgrade_actions.dirstatev2 in upgrade_op.upgrade_actions:
461 461 ui.status(_(b'upgrading to dirstate-v2 from v1\n'))
462 462 upgrade_dirstate(ui, srcrepo, upgrade_op, b'v1', b'v2')
463 463 upgrade_op.upgrade_actions.remove(upgrade_actions.dirstatev2)
464 464
465 465 if upgrade_actions.dirstatev2 in upgrade_op.removed_actions:
466 466 ui.status(_(b'downgrading from dirstate-v2 to v1\n'))
467 467 upgrade_dirstate(ui, srcrepo, upgrade_op, b'v2', b'v1')
468 468 upgrade_op.removed_actions.remove(upgrade_actions.dirstatev2)
469 469
470 470 if upgrade_actions.dirstatetrackedkey in upgrade_op.upgrade_actions:
471 471 ui.status(_(b'create dirstate-tracked-hint file\n'))
472 472 upgrade_tracked_hint(ui, srcrepo, upgrade_op, add=True)
473 473 upgrade_op.upgrade_actions.remove(upgrade_actions.dirstatetrackedkey)
474 474 elif upgrade_actions.dirstatetrackedkey in upgrade_op.removed_actions:
475 475 ui.status(_(b'remove dirstate-tracked-hint file\n'))
476 476 upgrade_tracked_hint(ui, srcrepo, upgrade_op, add=False)
477 477 upgrade_op.removed_actions.remove(upgrade_actions.dirstatetrackedkey)
478 478
479 479 if not (upgrade_op.upgrade_actions or upgrade_op.removed_actions):
480 480 return
481 481
482 482 if upgrade_op.requirements_only:
483 483 ui.status(_(b'upgrading repository requirements\n'))
484 484 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
485 485 # if there is only one action and that is persistent nodemap upgrade
486 486 # directly write the nodemap file and update requirements instead of going
487 487 # through the whole cloning process
488 488 elif (
489 489 len(upgrade_op.upgrade_actions) == 1
490 490 and b'persistent-nodemap' in upgrade_op.upgrade_actions_names
491 491 and not upgrade_op.removed_actions
492 492 ):
493 493 ui.status(
494 494 _(b'upgrading repository to use persistent nodemap feature\n')
495 495 )
496 496 with srcrepo.transaction(b'upgrade') as tr:
497 497 unfi = srcrepo.unfiltered()
498 498 cl = unfi.changelog
499 499 nodemap.persist_nodemap(tr, cl, force=True)
500 500 # we want to directly operate on the underlying revlog to force
501 501 # create a nodemap file. This is fine since this is upgrade code
502 502 # and it heavily relies on repository being revlog based
503 503 # hence accessing private attributes can be justified
504 504 nodemap.persist_nodemap(
505 505 tr, unfi.manifestlog._rootstore._revlog, force=True
506 506 )
507 507 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
508 508 elif (
509 509 len(upgrade_op.removed_actions) == 1
510 510 and [
511 511 x
512 512 for x in upgrade_op.removed_actions
513 513 if x.name == b'persistent-nodemap'
514 514 ]
515 515 and not upgrade_op.upgrade_actions
516 516 ):
517 517 ui.status(
518 518 _(b'downgrading repository to not use persistent nodemap feature\n')
519 519 )
520 520 with srcrepo.transaction(b'upgrade') as tr:
521 521 unfi = srcrepo.unfiltered()
522 522 cl = unfi.changelog
523 523 nodemap.delete_nodemap(tr, srcrepo, cl)
524 524 # check comment 20 lines above for accessing private attributes
525 525 nodemap.delete_nodemap(
526 526 tr, srcrepo, unfi.manifestlog._rootstore._revlog
527 527 )
528 528 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
529 529 else:
530 530 with dstrepo.transaction(b'upgrade') as tr:
531 531 _clonerevlogs(
532 532 ui,
533 533 srcrepo,
534 534 dstrepo,
535 535 tr,
536 536 upgrade_op,
537 537 )
538 538
539 539 # Now copy other files in the store directory.
540 540 for p in _files_to_copy_post_revlog_clone(srcrepo):
541 541 srcrepo.ui.status(_(b'copying %s\n') % p)
542 542 src = srcrepo.store.rawvfs.join(p)
543 543 dst = dstrepo.store.rawvfs.join(p)
544 544 util.copyfile(src, dst, copystat=True)
545 545
546 546 finishdatamigration(ui, srcrepo, dstrepo, requirements)
547 547
548 548 ui.status(_(b'data fully upgraded in a temporary repository\n'))
549 549
550 550 if upgrade_op.backup_store:
551 551 backuppath = pycompat.mkdtemp(
552 552 prefix=b'upgradebackup.', dir=srcrepo.path
553 553 )
554 554 backupvfs = vfsmod.vfs(backuppath)
555 555
556 556 # Make a backup of requires file first, as it is the first to be modified.
557 557 util.copyfile(
558 558 srcrepo.vfs.join(b'requires'), backupvfs.join(b'requires')
559 559 )
560 560
561 561 # We install an arbitrary requirement that clients must not support
562 562 # as a mechanism to lock out new clients during the data swap. This is
563 563 # better than allowing a client to continue while the repository is in
564 564 # an inconsistent state.
565 565 ui.status(
566 566 _(
567 567 b'marking source repository as being upgraded; clients will be '
568 568 b'unable to read from repository\n'
569 569 )
570 570 )
571 571 scmutil.writereporequirements(
572 572 srcrepo, srcrepo.requirements | {b'upgradeinprogress'}
573 573 )
574 574
575 575 ui.status(_(b'starting in-place swap of repository data\n'))
576 576 if upgrade_op.backup_store:
577 577 ui.status(
578 578 _(b'replaced files will be backed up at %s\n') % backuppath
579 579 )
580 580
581 581 # Now swap in the new store directory. Doing it as a rename should make
582 582 # the operation nearly instantaneous and atomic (at least in well-behaved
583 583 # environments).
584 584 ui.status(_(b'replacing store...\n'))
585 585 tstart = util.timer()
586 586 _replacestores(srcrepo, dstrepo, backupvfs, upgrade_op)
587 587 elapsed = util.timer() - tstart
588 588 ui.status(
589 589 _(
590 590 b'store replacement complete; repository was inconsistent for '
591 591 b'%0.1fs\n'
592 592 )
593 593 % elapsed
594 594 )
595 595
596 596 # We first write the requirements file. Any new requirements will lock
597 597 # out legacy clients.
598 598 ui.status(
599 599 _(
600 600 b'finalizing requirements file and making repository readable '
601 601 b'again\n'
602 602 )
603 603 )
604 604 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
605 605
606 606 if upgrade_op.backup_store:
607 607 # The lock file from the old store won't be removed because nothing has a
608 608 # reference to its new location. So clean it up manually. Alternatively, we
609 609 # could update srcrepo.svfs and other variables to point to the new
610 610 # location. This is simpler.
611 611 assert backupvfs is not None # help pytype
612 612 backupvfs.unlink(b'store/lock')
613 613
614 614 return backuppath
615 615
616 616
617 617 def upgrade_dirstate(ui, srcrepo, upgrade_op, old, new):
618 618 if upgrade_op.backup_store:
619 619 backuppath = pycompat.mkdtemp(
620 620 prefix=b'upgradebackup.', dir=srcrepo.path
621 621 )
622 622 ui.status(_(b'replaced files will be backed up at %s\n') % backuppath)
623 623 backupvfs = vfsmod.vfs(backuppath)
624 624 util.copyfile(
625 625 srcrepo.vfs.join(b'requires'), backupvfs.join(b'requires')
626 626 )
627 627 try:
628 628 util.copyfile(
629 629 srcrepo.vfs.join(b'dirstate'), backupvfs.join(b'dirstate')
630 630 )
631 631 except FileNotFoundError:
632 632 # The dirstate does not exist on an empty repo or a repo with no
633 633 # revision checked out
634 634 pass
635 635
636 636 assert srcrepo.dirstate._use_dirstate_v2 == (old == b'v2')
637 637 use_v2 = new == b'v2'
638 638 if use_v2:
639 639 # Write the requirements *before* upgrading
640 640 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
641 641
642 642 srcrepo.dirstate._map.preload()
643 643 srcrepo.dirstate._use_dirstate_v2 = use_v2
644 644 srcrepo.dirstate._map._use_dirstate_v2 = use_v2
645 645 srcrepo.dirstate._dirty = True
646 646 try:
647 647 srcrepo.vfs.unlink(b'dirstate')
648 648 except FileNotFoundError:
649 649 # The dirstate does not exist on an empty repo or a repo with no
650 650 # revision checked out
651 651 pass
652 652
653 653 srcrepo.dirstate.write(None)
654 654 if not use_v2:
655 655 # Remove the v2 requirement *after* downgrading
656 656 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
657 657
658 658
659 659 def upgrade_tracked_hint(ui, srcrepo, upgrade_op, add):
660 660 if add:
661 661 srcrepo.dirstate._use_tracked_hint = True
662 662 srcrepo.dirstate._dirty = True
663 663 srcrepo.dirstate._dirty_tracked_set = True
664 664 srcrepo.dirstate.write(None)
665 665 if not add:
666 666 srcrepo.dirstate.delete_tracked_hint()
667 667
668 668 scmutil.writereporequirements(srcrepo, upgrade_op.new_requirements)
General Comments 0
You need to be logged in to leave comments. Login now