##// END OF EJS Templates
sidedatacopies: deal with upgrading and downgrading to that format...
marmoute -
r43418:843da183 default
parent child Browse files
Show More
@@ -1,957 +1,1009 b''
1 1 # copies.py - copy detection for Mercurial
2 2 #
3 3 # Copyright 2008 Matt Mackall <mpm@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 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import heapq
12 12 import os
13 13
14 14 from .i18n import _
15 15
16
17 from .revlogutils.flagutil import REVIDX_SIDEDATA
18
16 19 from . import (
17 20 error,
18 21 match as matchmod,
19 22 node,
20 23 pathutil,
21 24 pycompat,
22 25 util,
23 26 )
27
28 from .revlogutils import sidedata as sidedatamod
29
24 30 from .utils import stringutil
25 31
26 32
27 33 def _findlimit(repo, ctxa, ctxb):
28 34 """
29 35 Find the last revision that needs to be checked to ensure that a full
30 36 transitive closure for file copies can be properly calculated.
31 37 Generally, this means finding the earliest revision number that's an
32 38 ancestor of a or b but not both, except when a or b is a direct descendent
33 39 of the other, in which case we can return the minimum revnum of a and b.
34 40 """
35 41
36 42 # basic idea:
37 43 # - mark a and b with different sides
38 44 # - if a parent's children are all on the same side, the parent is
39 45 # on that side, otherwise it is on no side
40 46 # - walk the graph in topological order with the help of a heap;
41 47 # - add unseen parents to side map
42 48 # - clear side of any parent that has children on different sides
43 49 # - track number of interesting revs that might still be on a side
44 50 # - track the lowest interesting rev seen
45 51 # - quit when interesting revs is zero
46 52
47 53 cl = repo.changelog
48 54 wdirparents = None
49 55 a = ctxa.rev()
50 56 b = ctxb.rev()
51 57 if a is None:
52 58 wdirparents = (ctxa.p1(), ctxa.p2())
53 59 a = node.wdirrev
54 60 if b is None:
55 61 assert not wdirparents
56 62 wdirparents = (ctxb.p1(), ctxb.p2())
57 63 b = node.wdirrev
58 64
59 65 side = {a: -1, b: 1}
60 66 visit = [-a, -b]
61 67 heapq.heapify(visit)
62 68 interesting = len(visit)
63 69 limit = node.wdirrev
64 70
65 71 while interesting:
66 72 r = -(heapq.heappop(visit))
67 73 if r == node.wdirrev:
68 74 parents = [pctx.rev() for pctx in wdirparents]
69 75 else:
70 76 parents = cl.parentrevs(r)
71 77 if parents[1] == node.nullrev:
72 78 parents = parents[:1]
73 79 for p in parents:
74 80 if p not in side:
75 81 # first time we see p; add it to visit
76 82 side[p] = side[r]
77 83 if side[p]:
78 84 interesting += 1
79 85 heapq.heappush(visit, -p)
80 86 elif side[p] and side[p] != side[r]:
81 87 # p was interesting but now we know better
82 88 side[p] = 0
83 89 interesting -= 1
84 90 if side[r]:
85 91 limit = r # lowest rev visited
86 92 interesting -= 1
87 93
88 94 # Consider the following flow (see test-commit-amend.t under issue4405):
89 95 # 1/ File 'a0' committed
90 96 # 2/ File renamed from 'a0' to 'a1' in a new commit (call it 'a1')
91 97 # 3/ Move back to first commit
92 98 # 4/ Create a new commit via revert to contents of 'a1' (call it 'a1-amend')
93 99 # 5/ Rename file from 'a1' to 'a2' and commit --amend 'a1-msg'
94 100 #
95 101 # During the amend in step five, we will be in this state:
96 102 #
97 103 # @ 3 temporary amend commit for a1-amend
98 104 # |
99 105 # o 2 a1-amend
100 106 # |
101 107 # | o 1 a1
102 108 # |/
103 109 # o 0 a0
104 110 #
105 111 # When _findlimit is called, a and b are revs 3 and 0, so limit will be 2,
106 112 # yet the filelog has the copy information in rev 1 and we will not look
107 113 # back far enough unless we also look at the a and b as candidates.
108 114 # This only occurs when a is a descendent of b or visa-versa.
109 115 return min(limit, a, b)
110 116
111 117
112 118 def _filter(src, dst, t):
113 119 """filters out invalid copies after chaining"""
114 120
115 121 # When _chain()'ing copies in 'a' (from 'src' via some other commit 'mid')
116 122 # with copies in 'b' (from 'mid' to 'dst'), we can get the different cases
117 123 # in the following table (not including trivial cases). For example, case 2
118 124 # is where a file existed in 'src' and remained under that name in 'mid' and
119 125 # then was renamed between 'mid' and 'dst'.
120 126 #
121 127 # case src mid dst result
122 128 # 1 x y - -
123 129 # 2 x y y x->y
124 130 # 3 x y x -
125 131 # 4 x y z x->z
126 132 # 5 - x y -
127 133 # 6 x x y x->y
128 134 #
129 135 # _chain() takes care of chaining the copies in 'a' and 'b', but it
130 136 # cannot tell the difference between cases 1 and 2, between 3 and 4, or
131 137 # between 5 and 6, so it includes all cases in its result.
132 138 # Cases 1, 3, and 5 are then removed by _filter().
133 139
134 140 for k, v in list(t.items()):
135 141 # remove copies from files that didn't exist
136 142 if v not in src:
137 143 del t[k]
138 144 # remove criss-crossed copies
139 145 elif k in src and v in dst:
140 146 del t[k]
141 147 # remove copies to files that were then removed
142 148 elif k not in dst:
143 149 del t[k]
144 150
145 151
146 152 def _chain(a, b):
147 153 """chain two sets of copies 'a' and 'b'"""
148 154 t = a.copy()
149 155 for k, v in pycompat.iteritems(b):
150 156 if v in t:
151 157 t[k] = t[v]
152 158 else:
153 159 t[k] = v
154 160 return t
155 161
156 162
157 163 def _tracefile(fctx, am, basemf, limit):
158 164 """return file context that is the ancestor of fctx present in ancestor
159 165 manifest am, stopping after the first ancestor lower than limit"""
160 166
161 167 for f in fctx.ancestors():
162 168 path = f.path()
163 169 if am.get(path, None) == f.filenode():
164 170 return path
165 171 if basemf and basemf.get(path, None) == f.filenode():
166 172 return path
167 173 if not f.isintroducedafter(limit):
168 174 return None
169 175
170 176
171 177 def _dirstatecopies(repo, match=None):
172 178 ds = repo.dirstate
173 179 c = ds.copies().copy()
174 180 for k in list(c):
175 181 if ds[k] not in b'anm' or (match and not match(k)):
176 182 del c[k]
177 183 return c
178 184
179 185
180 186 def _computeforwardmissing(a, b, match=None):
181 187 """Computes which files are in b but not a.
182 188 This is its own function so extensions can easily wrap this call to see what
183 189 files _forwardcopies is about to process.
184 190 """
185 191 ma = a.manifest()
186 192 mb = b.manifest()
187 193 return mb.filesnotin(ma, match=match)
188 194
189 195
190 196 def usechangesetcentricalgo(repo):
191 197 """Checks if we should use changeset-centric copy algorithms"""
192 198 if repo.filecopiesmode == b'changeset-sidedata':
193 199 return True
194 200 readfrom = repo.ui.config(b'experimental', b'copies.read-from')
195 201 changesetsource = (b'changeset-only', b'compatibility')
196 202 return readfrom in changesetsource
197 203
198 204
199 205 def _committedforwardcopies(a, b, base, match):
200 206 """Like _forwardcopies(), but b.rev() cannot be None (working copy)"""
201 207 # files might have to be traced back to the fctx parent of the last
202 208 # one-side-only changeset, but not further back than that
203 209 repo = a._repo
204 210
205 211 if usechangesetcentricalgo(repo):
206 212 return _changesetforwardcopies(a, b, match)
207 213
208 214 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
209 215 dbg = repo.ui.debug
210 216 if debug:
211 217 dbg(b'debug.copies: looking into rename from %s to %s\n' % (a, b))
212 218 limit = _findlimit(repo, a, b)
213 219 if debug:
214 220 dbg(b'debug.copies: search limit: %d\n' % limit)
215 221 am = a.manifest()
216 222 basemf = None if base is None else base.manifest()
217 223
218 224 # find where new files came from
219 225 # we currently don't try to find where old files went, too expensive
220 226 # this means we can miss a case like 'hg rm b; hg cp a b'
221 227 cm = {}
222 228
223 229 # Computing the forward missing is quite expensive on large manifests, since
224 230 # it compares the entire manifests. We can optimize it in the common use
225 231 # case of computing what copies are in a commit versus its parent (like
226 232 # during a rebase or histedit). Note, we exclude merge commits from this
227 233 # optimization, since the ctx.files() for a merge commit is not correct for
228 234 # this comparison.
229 235 forwardmissingmatch = match
230 236 if b.p1() == a and b.p2().node() == node.nullid:
231 237 filesmatcher = matchmod.exact(b.files())
232 238 forwardmissingmatch = matchmod.intersectmatchers(match, filesmatcher)
233 239 missing = _computeforwardmissing(a, b, match=forwardmissingmatch)
234 240
235 241 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
236 242
237 243 if debug:
238 244 dbg(b'debug.copies: missing files to search: %d\n' % len(missing))
239 245
240 246 for f in sorted(missing):
241 247 if debug:
242 248 dbg(b'debug.copies: tracing file: %s\n' % f)
243 249 fctx = b[f]
244 250 fctx._ancestrycontext = ancestrycontext
245 251
246 252 if debug:
247 253 start = util.timer()
248 254 opath = _tracefile(fctx, am, basemf, limit)
249 255 if opath:
250 256 if debug:
251 257 dbg(b'debug.copies: rename of: %s\n' % opath)
252 258 cm[f] = opath
253 259 if debug:
254 260 dbg(
255 261 b'debug.copies: time: %f seconds\n'
256 262 % (util.timer() - start)
257 263 )
258 264 return cm
259 265
260 266
261 267 def _changesetforwardcopies(a, b, match):
262 268 if a.rev() in (node.nullrev, b.rev()):
263 269 return {}
264 270
265 271 repo = a.repo()
266 272 children = {}
267 273 cl = repo.changelog
268 274 missingrevs = cl.findmissingrevs(common=[a.rev()], heads=[b.rev()])
269 275 for r in missingrevs:
270 276 for p in cl.parentrevs(r):
271 277 if p == node.nullrev:
272 278 continue
273 279 if p not in children:
274 280 children[p] = [r]
275 281 else:
276 282 children[p].append(r)
277 283
278 284 roots = set(children) - set(missingrevs)
279 285 # 'work' contains 3-tuples of a (revision number, parent number, copies).
280 286 # The parent number is only used for knowing which parent the copies dict
281 287 # came from.
282 288 # NOTE: To reduce costly copying the 'copies' dicts, we reuse the same
283 289 # instance for *one* of the child nodes (the last one). Once an instance
284 290 # has been put on the queue, it is thus no longer safe to modify it.
285 291 # Conversely, it *is* safe to modify an instance popped off the queue.
286 292 work = [(r, 1, {}) for r in roots]
287 293 heapq.heapify(work)
288 294 alwaysmatch = match.always()
289 295 while work:
290 296 r, i1, copies = heapq.heappop(work)
291 297 if work and work[0][0] == r:
292 298 # We are tracing copies from both parents
293 299 r, i2, copies2 = heapq.heappop(work)
294 300 for dst, src in copies2.items():
295 301 # Unlike when copies are stored in the filelog, we consider
296 302 # it a copy even if the destination already existed on the
297 303 # other branch. It's simply too expensive to check if the
298 304 # file existed in the manifest.
299 305 if dst not in copies:
300 306 # If it was copied on the p1 side, leave it as copied from
301 307 # that side, even if it was also copied on the p2 side.
302 308 copies[dst] = copies2[dst]
303 309 if r == b.rev():
304 310 return copies
305 311 for i, c in enumerate(children[r]):
306 312 childctx = repo[c]
307 313 if r == childctx.p1().rev():
308 314 parent = 1
309 315 childcopies = childctx.p1copies()
310 316 else:
311 317 assert r == childctx.p2().rev()
312 318 parent = 2
313 319 childcopies = childctx.p2copies()
314 320 if not alwaysmatch:
315 321 childcopies = {
316 322 dst: src for dst, src in childcopies.items() if match(dst)
317 323 }
318 324 # Copy the dict only if later iterations will also need it
319 325 if i != len(children[r]) - 1:
320 326 newcopies = copies.copy()
321 327 else:
322 328 newcopies = copies
323 329 if childcopies:
324 330 newcopies = _chain(newcopies, childcopies)
325 331 for f in childctx.filesremoved():
326 332 if f in newcopies:
327 333 del newcopies[f]
328 334 heapq.heappush(work, (c, parent, newcopies))
329 335 assert False
330 336
331 337
332 338 def _forwardcopies(a, b, base=None, match=None):
333 339 """find {dst@b: src@a} copy mapping where a is an ancestor of b"""
334 340
335 341 if base is None:
336 342 base = a
337 343 match = a.repo().narrowmatch(match)
338 344 # check for working copy
339 345 if b.rev() is None:
340 346 cm = _committedforwardcopies(a, b.p1(), base, match)
341 347 # combine copies from dirstate if necessary
342 348 copies = _chain(cm, _dirstatecopies(b._repo, match))
343 349 else:
344 350 copies = _committedforwardcopies(a, b, base, match)
345 351 return copies
346 352
347 353
348 354 def _backwardrenames(a, b, match):
349 355 if a._repo.ui.config(b'experimental', b'copytrace') == b'off':
350 356 return {}
351 357
352 358 # Even though we're not taking copies into account, 1:n rename situations
353 359 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
354 360 # arbitrarily pick one of the renames.
355 361 # We don't want to pass in "match" here, since that would filter
356 362 # the destination by it. Since we're reversing the copies, we want
357 363 # to filter the source instead.
358 364 f = _forwardcopies(b, a)
359 365 r = {}
360 366 for k, v in sorted(pycompat.iteritems(f)):
361 367 if match and not match(v):
362 368 continue
363 369 # remove copies
364 370 if v in a:
365 371 continue
366 372 r[v] = k
367 373 return r
368 374
369 375
370 376 def pathcopies(x, y, match=None):
371 377 """find {dst@y: src@x} copy mapping for directed compare"""
372 378 repo = x._repo
373 379 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
374 380 if debug:
375 381 repo.ui.debug(
376 382 b'debug.copies: searching copies from %s to %s\n' % (x, y)
377 383 )
378 384 if x == y or not x or not y:
379 385 return {}
380 386 a = y.ancestor(x)
381 387 if a == x:
382 388 if debug:
383 389 repo.ui.debug(b'debug.copies: search mode: forward\n')
384 390 if y.rev() is None and x == y.p1():
385 391 # short-circuit to avoid issues with merge states
386 392 return _dirstatecopies(repo, match)
387 393 copies = _forwardcopies(x, y, match=match)
388 394 elif a == y:
389 395 if debug:
390 396 repo.ui.debug(b'debug.copies: search mode: backward\n')
391 397 copies = _backwardrenames(x, y, match=match)
392 398 else:
393 399 if debug:
394 400 repo.ui.debug(b'debug.copies: search mode: combined\n')
395 401 base = None
396 402 if a.rev() != node.nullrev:
397 403 base = x
398 404 copies = _chain(
399 405 _backwardrenames(x, a, match=match),
400 406 _forwardcopies(a, y, base, match=match),
401 407 )
402 408 _filter(x, y, copies)
403 409 return copies
404 410
405 411
406 412 def mergecopies(repo, c1, c2, base):
407 413 """
408 414 Finds moves and copies between context c1 and c2 that are relevant for
409 415 merging. 'base' will be used as the merge base.
410 416
411 417 Copytracing is used in commands like rebase, merge, unshelve, etc to merge
412 418 files that were moved/ copied in one merge parent and modified in another.
413 419 For example:
414 420
415 421 o ---> 4 another commit
416 422 |
417 423 | o ---> 3 commit that modifies a.txt
418 424 | /
419 425 o / ---> 2 commit that moves a.txt to b.txt
420 426 |/
421 427 o ---> 1 merge base
422 428
423 429 If we try to rebase revision 3 on revision 4, since there is no a.txt in
424 430 revision 4, and if user have copytrace disabled, we prints the following
425 431 message:
426 432
427 433 ```other changed <file> which local deleted```
428 434
429 435 Returns five dicts: "copy", "movewithdir", "diverge", "renamedelete" and
430 436 "dirmove".
431 437
432 438 "copy" is a mapping from destination name -> source name,
433 439 where source is in c1 and destination is in c2 or vice-versa.
434 440
435 441 "movewithdir" is a mapping from source name -> destination name,
436 442 where the file at source present in one context but not the other
437 443 needs to be moved to destination by the merge process, because the
438 444 other context moved the directory it is in.
439 445
440 446 "diverge" is a mapping of source name -> list of destination names
441 447 for divergent renames.
442 448
443 449 "renamedelete" is a mapping of source name -> list of destination
444 450 names for files deleted in c1 that were renamed in c2 or vice-versa.
445 451
446 452 "dirmove" is a mapping of detected source dir -> destination dir renames.
447 453 This is needed for handling changes to new files previously grafted into
448 454 renamed directories.
449 455
450 456 This function calls different copytracing algorithms based on config.
451 457 """
452 458 # avoid silly behavior for update from empty dir
453 459 if not c1 or not c2 or c1 == c2:
454 460 return {}, {}, {}, {}, {}
455 461
456 462 narrowmatch = c1.repo().narrowmatch()
457 463
458 464 # avoid silly behavior for parent -> working dir
459 465 if c2.node() is None and c1.node() == repo.dirstate.p1():
460 466 return _dirstatecopies(repo, narrowmatch), {}, {}, {}, {}
461 467
462 468 copytracing = repo.ui.config(b'experimental', b'copytrace')
463 469 if stringutil.parsebool(copytracing) is False:
464 470 # stringutil.parsebool() returns None when it is unable to parse the
465 471 # value, so we should rely on making sure copytracing is on such cases
466 472 return {}, {}, {}, {}, {}
467 473
468 474 if usechangesetcentricalgo(repo):
469 475 # The heuristics don't make sense when we need changeset-centric algos
470 476 return _fullcopytracing(repo, c1, c2, base)
471 477
472 478 # Copy trace disabling is explicitly below the node == p1 logic above
473 479 # because the logic above is required for a simple copy to be kept across a
474 480 # rebase.
475 481 if copytracing == b'heuristics':
476 482 # Do full copytracing if only non-public revisions are involved as
477 483 # that will be fast enough and will also cover the copies which could
478 484 # be missed by heuristics
479 485 if _isfullcopytraceable(repo, c1, base):
480 486 return _fullcopytracing(repo, c1, c2, base)
481 487 return _heuristicscopytracing(repo, c1, c2, base)
482 488 else:
483 489 return _fullcopytracing(repo, c1, c2, base)
484 490
485 491
486 492 def _isfullcopytraceable(repo, c1, base):
487 493 """ Checks that if base, source and destination are all no-public branches,
488 494 if yes let's use the full copytrace algorithm for increased capabilities
489 495 since it will be fast enough.
490 496
491 497 `experimental.copytrace.sourcecommitlimit` can be used to set a limit for
492 498 number of changesets from c1 to base such that if number of changesets are
493 499 more than the limit, full copytracing algorithm won't be used.
494 500 """
495 501 if c1.rev() is None:
496 502 c1 = c1.p1()
497 503 if c1.mutable() and base.mutable():
498 504 sourcecommitlimit = repo.ui.configint(
499 505 b'experimental', b'copytrace.sourcecommitlimit'
500 506 )
501 507 commits = len(repo.revs(b'%d::%d', base.rev(), c1.rev()))
502 508 return commits < sourcecommitlimit
503 509 return False
504 510
505 511
506 512 def _checksinglesidecopies(
507 513 src, dsts1, m1, m2, mb, c2, base, copy, renamedelete
508 514 ):
509 515 if src not in m2:
510 516 # deleted on side 2
511 517 if src not in m1:
512 518 # renamed on side 1, deleted on side 2
513 519 renamedelete[src] = dsts1
514 520 elif m2[src] != mb[src]:
515 521 if not _related(c2[src], base[src]):
516 522 return
517 523 # modified on side 2
518 524 for dst in dsts1:
519 525 if dst not in m2:
520 526 # dst not added on side 2 (handle as regular
521 527 # "both created" case in manifestmerge otherwise)
522 528 copy[dst] = src
523 529
524 530
525 531 def _fullcopytracing(repo, c1, c2, base):
526 532 """ The full copytracing algorithm which finds all the new files that were
527 533 added from merge base up to the top commit and for each file it checks if
528 534 this file was copied from another file.
529 535
530 536 This is pretty slow when a lot of changesets are involved but will track all
531 537 the copies.
532 538 """
533 539 m1 = c1.manifest()
534 540 m2 = c2.manifest()
535 541 mb = base.manifest()
536 542
537 543 copies1 = pathcopies(base, c1)
538 544 copies2 = pathcopies(base, c2)
539 545
540 546 inversecopies1 = {}
541 547 inversecopies2 = {}
542 548 for dst, src in copies1.items():
543 549 inversecopies1.setdefault(src, []).append(dst)
544 550 for dst, src in copies2.items():
545 551 inversecopies2.setdefault(src, []).append(dst)
546 552
547 553 copy = {}
548 554 diverge = {}
549 555 renamedelete = {}
550 556 allsources = set(inversecopies1) | set(inversecopies2)
551 557 for src in allsources:
552 558 dsts1 = inversecopies1.get(src)
553 559 dsts2 = inversecopies2.get(src)
554 560 if dsts1 and dsts2:
555 561 # copied/renamed on both sides
556 562 if src not in m1 and src not in m2:
557 563 # renamed on both sides
558 564 dsts1 = set(dsts1)
559 565 dsts2 = set(dsts2)
560 566 # If there's some overlap in the rename destinations, we
561 567 # consider it not divergent. For example, if side 1 copies 'a'
562 568 # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c'
563 569 # and 'd' and deletes 'a'.
564 570 if dsts1 & dsts2:
565 571 for dst in dsts1 & dsts2:
566 572 copy[dst] = src
567 573 else:
568 574 diverge[src] = sorted(dsts1 | dsts2)
569 575 elif src in m1 and src in m2:
570 576 # copied on both sides
571 577 dsts1 = set(dsts1)
572 578 dsts2 = set(dsts2)
573 579 for dst in dsts1 & dsts2:
574 580 copy[dst] = src
575 581 # TODO: Handle cases where it was renamed on one side and copied
576 582 # on the other side
577 583 elif dsts1:
578 584 # copied/renamed only on side 1
579 585 _checksinglesidecopies(
580 586 src, dsts1, m1, m2, mb, c2, base, copy, renamedelete
581 587 )
582 588 elif dsts2:
583 589 # copied/renamed only on side 2
584 590 _checksinglesidecopies(
585 591 src, dsts2, m2, m1, mb, c1, base, copy, renamedelete
586 592 )
587 593
588 594 renamedeleteset = set()
589 595 divergeset = set()
590 596 for dsts in diverge.values():
591 597 divergeset.update(dsts)
592 598 for dsts in renamedelete.values():
593 599 renamedeleteset.update(dsts)
594 600
595 601 # find interesting file sets from manifests
596 602 addedinm1 = m1.filesnotin(mb, repo.narrowmatch())
597 603 addedinm2 = m2.filesnotin(mb, repo.narrowmatch())
598 604 u1 = sorted(addedinm1 - addedinm2)
599 605 u2 = sorted(addedinm2 - addedinm1)
600 606
601 607 header = b" unmatched files in %s"
602 608 if u1:
603 609 repo.ui.debug(b"%s:\n %s\n" % (header % b'local', b"\n ".join(u1)))
604 610 if u2:
605 611 repo.ui.debug(b"%s:\n %s\n" % (header % b'other', b"\n ".join(u2)))
606 612
607 613 fullcopy = copies1.copy()
608 614 fullcopy.update(copies2)
609 615 if not fullcopy:
610 616 return copy, {}, diverge, renamedelete, {}
611 617
612 618 if repo.ui.debugflag:
613 619 repo.ui.debug(
614 620 b" all copies found (* = to merge, ! = divergent, "
615 621 b"% = renamed and deleted):\n"
616 622 )
617 623 for f in sorted(fullcopy):
618 624 note = b""
619 625 if f in copy:
620 626 note += b"*"
621 627 if f in divergeset:
622 628 note += b"!"
623 629 if f in renamedeleteset:
624 630 note += b"%"
625 631 repo.ui.debug(
626 632 b" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f, note)
627 633 )
628 634 del divergeset
629 635
630 636 repo.ui.debug(b" checking for directory renames\n")
631 637
632 638 # generate a directory move map
633 639 d1, d2 = c1.dirs(), c2.dirs()
634 640 invalid = set()
635 641 dirmove = {}
636 642
637 643 # examine each file copy for a potential directory move, which is
638 644 # when all the files in a directory are moved to a new directory
639 645 for dst, src in pycompat.iteritems(fullcopy):
640 646 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
641 647 if dsrc in invalid:
642 648 # already seen to be uninteresting
643 649 continue
644 650 elif dsrc in d1 and ddst in d1:
645 651 # directory wasn't entirely moved locally
646 652 invalid.add(dsrc)
647 653 elif dsrc in d2 and ddst in d2:
648 654 # directory wasn't entirely moved remotely
649 655 invalid.add(dsrc)
650 656 elif dsrc in dirmove and dirmove[dsrc] != ddst:
651 657 # files from the same directory moved to two different places
652 658 invalid.add(dsrc)
653 659 else:
654 660 # looks good so far
655 661 dirmove[dsrc] = ddst
656 662
657 663 for i in invalid:
658 664 if i in dirmove:
659 665 del dirmove[i]
660 666 del d1, d2, invalid
661 667
662 668 if not dirmove:
663 669 return copy, {}, diverge, renamedelete, {}
664 670
665 671 dirmove = {k + b"/": v + b"/" for k, v in pycompat.iteritems(dirmove)}
666 672
667 673 for d in dirmove:
668 674 repo.ui.debug(
669 675 b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d])
670 676 )
671 677
672 678 movewithdir = {}
673 679 # check unaccounted nonoverlapping files against directory moves
674 680 for f in u1 + u2:
675 681 if f not in fullcopy:
676 682 for d in dirmove:
677 683 if f.startswith(d):
678 684 # new file added in a directory that was moved, move it
679 685 df = dirmove[d] + f[len(d) :]
680 686 if df not in copy:
681 687 movewithdir[f] = df
682 688 repo.ui.debug(
683 689 b" pending file src: '%s' -> dst: '%s'\n"
684 690 % (f, df)
685 691 )
686 692 break
687 693
688 694 return copy, movewithdir, diverge, renamedelete, dirmove
689 695
690 696
691 697 def _heuristicscopytracing(repo, c1, c2, base):
692 698 """ Fast copytracing using filename heuristics
693 699
694 700 Assumes that moves or renames are of following two types:
695 701
696 702 1) Inside a directory only (same directory name but different filenames)
697 703 2) Move from one directory to another
698 704 (same filenames but different directory names)
699 705
700 706 Works only when there are no merge commits in the "source branch".
701 707 Source branch is commits from base up to c2 not including base.
702 708
703 709 If merge is involved it fallbacks to _fullcopytracing().
704 710
705 711 Can be used by setting the following config:
706 712
707 713 [experimental]
708 714 copytrace = heuristics
709 715
710 716 In some cases the copy/move candidates found by heuristics can be very large
711 717 in number and that will make the algorithm slow. The number of possible
712 718 candidates to check can be limited by using the config
713 719 `experimental.copytrace.movecandidateslimit` which defaults to 100.
714 720 """
715 721
716 722 if c1.rev() is None:
717 723 c1 = c1.p1()
718 724 if c2.rev() is None:
719 725 c2 = c2.p1()
720 726
721 727 copies = {}
722 728
723 729 changedfiles = set()
724 730 m1 = c1.manifest()
725 731 if not repo.revs(b'%d::%d', base.rev(), c2.rev()):
726 732 # If base is not in c2 branch, we switch to fullcopytracing
727 733 repo.ui.debug(
728 734 b"switching to full copytracing as base is not "
729 735 b"an ancestor of c2\n"
730 736 )
731 737 return _fullcopytracing(repo, c1, c2, base)
732 738
733 739 ctx = c2
734 740 while ctx != base:
735 741 if len(ctx.parents()) == 2:
736 742 # To keep things simple let's not handle merges
737 743 repo.ui.debug(b"switching to full copytracing because of merges\n")
738 744 return _fullcopytracing(repo, c1, c2, base)
739 745 changedfiles.update(ctx.files())
740 746 ctx = ctx.p1()
741 747
742 748 cp = _forwardcopies(base, c2)
743 749 for dst, src in pycompat.iteritems(cp):
744 750 if src in m1:
745 751 copies[dst] = src
746 752
747 753 # file is missing if it isn't present in the destination, but is present in
748 754 # the base and present in the source.
749 755 # Presence in the base is important to exclude added files, presence in the
750 756 # source is important to exclude removed files.
751 757 filt = lambda f: f not in m1 and f in base and f in c2
752 758 missingfiles = [f for f in changedfiles if filt(f)]
753 759
754 760 if missingfiles:
755 761 basenametofilename = collections.defaultdict(list)
756 762 dirnametofilename = collections.defaultdict(list)
757 763
758 764 for f in m1.filesnotin(base.manifest()):
759 765 basename = os.path.basename(f)
760 766 dirname = os.path.dirname(f)
761 767 basenametofilename[basename].append(f)
762 768 dirnametofilename[dirname].append(f)
763 769
764 770 for f in missingfiles:
765 771 basename = os.path.basename(f)
766 772 dirname = os.path.dirname(f)
767 773 samebasename = basenametofilename[basename]
768 774 samedirname = dirnametofilename[dirname]
769 775 movecandidates = samebasename + samedirname
770 776 # f is guaranteed to be present in c2, that's why
771 777 # c2.filectx(f) won't fail
772 778 f2 = c2.filectx(f)
773 779 # we can have a lot of candidates which can slow down the heuristics
774 780 # config value to limit the number of candidates moves to check
775 781 maxcandidates = repo.ui.configint(
776 782 b'experimental', b'copytrace.movecandidateslimit'
777 783 )
778 784
779 785 if len(movecandidates) > maxcandidates:
780 786 repo.ui.status(
781 787 _(
782 788 b"skipping copytracing for '%s', more "
783 789 b"candidates than the limit: %d\n"
784 790 )
785 791 % (f, len(movecandidates))
786 792 )
787 793 continue
788 794
789 795 for candidate in movecandidates:
790 796 f1 = c1.filectx(candidate)
791 797 if _related(f1, f2):
792 798 # if there are a few related copies then we'll merge
793 799 # changes into all of them. This matches the behaviour
794 800 # of upstream copytracing
795 801 copies[candidate] = f
796 802
797 803 return copies, {}, {}, {}, {}
798 804
799 805
800 806 def _related(f1, f2):
801 807 """return True if f1 and f2 filectx have a common ancestor
802 808
803 809 Walk back to common ancestor to see if the two files originate
804 810 from the same file. Since workingfilectx's rev() is None it messes
805 811 up the integer comparison logic, hence the pre-step check for
806 812 None (f1 and f2 can only be workingfilectx's initially).
807 813 """
808 814
809 815 if f1 == f2:
810 816 return True # a match
811 817
812 818 g1, g2 = f1.ancestors(), f2.ancestors()
813 819 try:
814 820 f1r, f2r = f1.linkrev(), f2.linkrev()
815 821
816 822 if f1r is None:
817 823 f1 = next(g1)
818 824 if f2r is None:
819 825 f2 = next(g2)
820 826
821 827 while True:
822 828 f1r, f2r = f1.linkrev(), f2.linkrev()
823 829 if f1r > f2r:
824 830 f1 = next(g1)
825 831 elif f2r > f1r:
826 832 f2 = next(g2)
827 833 else: # f1 and f2 point to files in the same linkrev
828 834 return f1 == f2 # true if they point to the same file
829 835 except StopIteration:
830 836 return False
831 837
832 838
833 839 def duplicatecopies(repo, wctx, rev, fromrev, skiprev=None):
834 840 """reproduce copies from fromrev to rev in the dirstate
835 841
836 842 If skiprev is specified, it's a revision that should be used to
837 843 filter copy records. Any copies that occur between fromrev and
838 844 skiprev will not be duplicated, even if they appear in the set of
839 845 copies between fromrev and rev.
840 846 """
841 847 exclude = {}
842 848 ctraceconfig = repo.ui.config(b'experimental', b'copytrace')
843 849 bctrace = stringutil.parsebool(ctraceconfig)
844 850 if skiprev is not None and (
845 851 ctraceconfig == b'heuristics' or bctrace or bctrace is None
846 852 ):
847 853 # copytrace='off' skips this line, but not the entire function because
848 854 # the line below is O(size of the repo) during a rebase, while the rest
849 855 # of the function is much faster (and is required for carrying copy
850 856 # metadata across the rebase anyway).
851 857 exclude = pathcopies(repo[fromrev], repo[skiprev])
852 858 for dst, src in pycompat.iteritems(pathcopies(repo[fromrev], repo[rev])):
853 859 if dst in exclude:
854 860 continue
855 861 if dst in wctx:
856 862 wctx[dst].markcopied(src)
857 863
858 864
859 865 def computechangesetfilesadded(ctx):
860 866 """return the list of files added in a changeset
861 867 """
862 868 added = []
863 869 for f in ctx.files():
864 870 if not any(f in p for p in ctx.parents()):
865 871 added.append(f)
866 872 return added
867 873
868 874
869 875 def computechangesetfilesremoved(ctx):
870 876 """return the list of files removed in a changeset
871 877 """
872 878 removed = []
873 879 for f in ctx.files():
874 880 if f not in ctx:
875 881 removed.append(f)
876 882 return removed
877 883
878 884
879 885 def computechangesetcopies(ctx):
880 886 """return the copies data for a changeset
881 887
882 888 The copies data are returned as a pair of dictionnary (p1copies, p2copies).
883 889
884 890 Each dictionnary are in the form: `{newname: oldname}`
885 891 """
886 892 p1copies = {}
887 893 p2copies = {}
888 894 p1 = ctx.p1()
889 895 p2 = ctx.p2()
890 896 narrowmatch = ctx._repo.narrowmatch()
891 897 for dst in ctx.files():
892 898 if not narrowmatch(dst) or dst not in ctx:
893 899 continue
894 900 copied = ctx[dst].renamed()
895 901 if not copied:
896 902 continue
897 903 src, srcnode = copied
898 904 if src in p1 and p1[src].filenode() == srcnode:
899 905 p1copies[dst] = src
900 906 elif src in p2 and p2[src].filenode() == srcnode:
901 907 p2copies[dst] = src
902 908 return p1copies, p2copies
903 909
904 910
905 911 def encodecopies(files, copies):
906 912 items = []
907 913 for i, dst in enumerate(files):
908 914 if dst in copies:
909 915 items.append(b'%d\0%s' % (i, copies[dst]))
910 916 if len(items) != len(copies):
911 917 raise error.ProgrammingError(
912 918 b'some copy targets missing from file list'
913 919 )
914 920 return b"\n".join(items)
915 921
916 922
917 923 def decodecopies(files, data):
918 924 try:
919 925 copies = {}
920 926 if not data:
921 927 return copies
922 928 for l in data.split(b'\n'):
923 929 strindex, src = l.split(b'\0')
924 930 i = int(strindex)
925 931 dst = files[i]
926 932 copies[dst] = src
927 933 return copies
928 934 except (ValueError, IndexError):
929 935 # Perhaps someone had chosen the same key name (e.g. "p1copies") and
930 936 # used different syntax for the value.
931 937 return None
932 938
933 939
934 940 def encodefileindices(files, subset):
935 941 subset = set(subset)
936 942 indices = []
937 943 for i, f in enumerate(files):
938 944 if f in subset:
939 945 indices.append(b'%d' % i)
940 946 return b'\n'.join(indices)
941 947
942 948
943 949 def decodefileindices(files, data):
944 950 try:
945 951 subset = []
946 952 if not data:
947 953 return subset
948 954 for strindex in data.split(b'\n'):
949 955 i = int(strindex)
950 956 if i < 0 or i >= len(files):
951 957 return None
952 958 subset.append(files[i])
953 959 return subset
954 960 except (ValueError, IndexError):
955 961 # Perhaps someone had chosen the same key name (e.g. "added") and
956 962 # used different syntax for the value.
957 963 return None
964
965
966 def _getsidedata(srcrepo, rev):
967 ctx = srcrepo[rev]
968 filescopies = computechangesetcopies(ctx)
969 filesadded = computechangesetfilesadded(ctx)
970 filesremoved = computechangesetfilesremoved(ctx)
971 sidedata = {}
972 if any([filescopies, filesadded, filesremoved]):
973 sortedfiles = sorted(ctx.files())
974 p1copies, p2copies = filescopies
975 p1copies = encodecopies(sortedfiles, p1copies)
976 p2copies = encodecopies(sortedfiles, p2copies)
977 filesadded = encodefileindices(sortedfiles, filesadded)
978 filesremoved = encodefileindices(sortedfiles, filesremoved)
979 sidedata[sidedatamod.SD_P1COPIES] = p1copies
980 sidedata[sidedatamod.SD_P2COPIES] = p2copies
981 sidedata[sidedatamod.SD_FILESADDED] = filesadded
982 sidedata[sidedatamod.SD_FILESREMOVED] = filesremoved
983 return sidedata
984
985
986 def getsidedataadder(srcrepo, destrepo):
987 def sidedatacompanion(revlog, rev):
988 sidedata = {}
989 if util.safehasattr(revlog, 'filteredrevs'): # this is a changelog
990 sidedata = _getsidedata(srcrepo, rev)
991 return False, (), sidedata
992
993 return sidedatacompanion
994
995
996 def getsidedataremover(srcrepo, destrepo):
997 def sidedatacompanion(revlog, rev):
998 f = ()
999 if util.safehasattr(revlog, 'filteredrevs'): # this is a changelog
1000 if revlog.flags(rev) & REVIDX_SIDEDATA:
1001 f = (
1002 sidedatamod.SD_P1COPIES,
1003 sidedatamod.SD_P2COPIES,
1004 sidedatamod.SD_FILESADDED,
1005 sidedatamod.SD_FILESREMOVED,
1006 )
1007 return False, f, {}
1008
1009 return sidedatacompanion
@@ -1,1379 +1,1387 b''
1 1 # upgrade.py - functions for in place upgrade of Mercurial repository
2 2 #
3 3 # Copyright (c) 2016-present, Gregory Szorc
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import stat
11 11
12 12 from .i18n import _
13 13 from .pycompat import getattr
14 14 from . import (
15 15 changelog,
16 copies,
16 17 error,
17 18 filelog,
18 19 hg,
19 20 localrepo,
20 21 manifest,
21 22 pycompat,
22 23 revlog,
23 24 scmutil,
24 25 util,
25 26 vfs as vfsmod,
26 27 )
27 28
28 29 from .utils import compression
29 30
30 31 # list of requirements that request a clone of all revlog if added/removed
31 32 RECLONES_REQUIREMENTS = {
32 33 b'generaldelta',
33 34 localrepo.SPARSEREVLOG_REQUIREMENT,
34 localrepo.SIDEDATA_REQUIREMENT,
35 35 }
36 36
37 37
38 38 def requiredsourcerequirements(repo):
39 39 """Obtain requirements required to be present to upgrade a repo.
40 40
41 41 An upgrade will not be allowed if the repository doesn't have the
42 42 requirements returned by this function.
43 43 """
44 44 return {
45 45 # Introduced in Mercurial 0.9.2.
46 46 b'revlogv1',
47 47 # Introduced in Mercurial 0.9.2.
48 48 b'store',
49 49 }
50 50
51 51
52 52 def blocksourcerequirements(repo):
53 53 """Obtain requirements that will prevent an upgrade from occurring.
54 54
55 55 An upgrade cannot be performed if the source repository contains a
56 56 requirements in the returned set.
57 57 """
58 58 return {
59 59 # The upgrade code does not yet support these experimental features.
60 60 # This is an artificial limitation.
61 61 b'treemanifest',
62 62 # This was a precursor to generaldelta and was never enabled by default.
63 63 # It should (hopefully) not exist in the wild.
64 64 b'parentdelta',
65 65 # Upgrade should operate on the actual store, not the shared link.
66 66 b'shared',
67 67 }
68 68
69 69
70 70 def supportremovedrequirements(repo):
71 71 """Obtain requirements that can be removed during an upgrade.
72 72
73 73 If an upgrade were to create a repository that dropped a requirement,
74 74 the dropped requirement must appear in the returned set for the upgrade
75 75 to be allowed.
76 76 """
77 77 supported = {
78 78 localrepo.SPARSEREVLOG_REQUIREMENT,
79 79 localrepo.SIDEDATA_REQUIREMENT,
80 localrepo.COPIESSDC_REQUIREMENT,
80 81 }
81 82 for name in compression.compengines:
82 83 engine = compression.compengines[name]
83 84 if engine.available() and engine.revlogheader():
84 85 supported.add(b'exp-compression-%s' % name)
85 86 if engine.name() == b'zstd':
86 87 supported.add(b'revlog-compression-zstd')
87 88 return supported
88 89
89 90
90 91 def supporteddestrequirements(repo):
91 92 """Obtain requirements that upgrade supports in the destination.
92 93
93 94 If the result of the upgrade would create requirements not in this set,
94 95 the upgrade is disallowed.
95 96
96 97 Extensions should monkeypatch this to add their custom requirements.
97 98 """
98 99 supported = {
99 100 b'dotencode',
100 101 b'fncache',
101 102 b'generaldelta',
102 103 b'revlogv1',
103 104 b'store',
104 105 localrepo.SPARSEREVLOG_REQUIREMENT,
105 106 localrepo.SIDEDATA_REQUIREMENT,
107 localrepo.COPIESSDC_REQUIREMENT,
106 108 }
107 109 for name in compression.compengines:
108 110 engine = compression.compengines[name]
109 111 if engine.available() and engine.revlogheader():
110 112 supported.add(b'exp-compression-%s' % name)
111 113 if engine.name() == b'zstd':
112 114 supported.add(b'revlog-compression-zstd')
113 115 return supported
114 116
115 117
116 118 def allowednewrequirements(repo):
117 119 """Obtain requirements that can be added to a repository during upgrade.
118 120
119 121 This is used to disallow proposed requirements from being added when
120 122 they weren't present before.
121 123
122 124 We use a list of allowed requirement additions instead of a list of known
123 125 bad additions because the whitelist approach is safer and will prevent
124 126 future, unknown requirements from accidentally being added.
125 127 """
126 128 supported = {
127 129 b'dotencode',
128 130 b'fncache',
129 131 b'generaldelta',
130 132 localrepo.SPARSEREVLOG_REQUIREMENT,
131 133 localrepo.SIDEDATA_REQUIREMENT,
134 localrepo.COPIESSDC_REQUIREMENT,
132 135 }
133 136 for name in compression.compengines:
134 137 engine = compression.compengines[name]
135 138 if engine.available() and engine.revlogheader():
136 139 supported.add(b'exp-compression-%s' % name)
137 140 if engine.name() == b'zstd':
138 141 supported.add(b'revlog-compression-zstd')
139 142 return supported
140 143
141 144
142 145 def preservedrequirements(repo):
143 146 return set()
144 147
145 148
146 149 deficiency = b'deficiency'
147 150 optimisation = b'optimization'
148 151
149 152
150 153 class improvement(object):
151 154 """Represents an improvement that can be made as part of an upgrade.
152 155
153 156 The following attributes are defined on each instance:
154 157
155 158 name
156 159 Machine-readable string uniquely identifying this improvement. It
157 160 will be mapped to an action later in the upgrade process.
158 161
159 162 type
160 163 Either ``deficiency`` or ``optimisation``. A deficiency is an obvious
161 164 problem. An optimization is an action (sometimes optional) that
162 165 can be taken to further improve the state of the repository.
163 166
164 167 description
165 168 Message intended for humans explaining the improvement in more detail,
166 169 including the implications of it. For ``deficiency`` types, should be
167 170 worded in the present tense. For ``optimisation`` types, should be
168 171 worded in the future tense.
169 172
170 173 upgrademessage
171 174 Message intended for humans explaining what an upgrade addressing this
172 175 issue will do. Should be worded in the future tense.
173 176 """
174 177
175 178 def __init__(self, name, type, description, upgrademessage):
176 179 self.name = name
177 180 self.type = type
178 181 self.description = description
179 182 self.upgrademessage = upgrademessage
180 183
181 184 def __eq__(self, other):
182 185 if not isinstance(other, improvement):
183 186 # This is what python tell use to do
184 187 return NotImplemented
185 188 return self.name == other.name
186 189
187 190 def __ne__(self, other):
188 191 return not (self == other)
189 192
190 193 def __hash__(self):
191 194 return hash(self.name)
192 195
193 196
194 197 allformatvariant = []
195 198
196 199
197 200 def registerformatvariant(cls):
198 201 allformatvariant.append(cls)
199 202 return cls
200 203
201 204
202 205 class formatvariant(improvement):
203 206 """an improvement subclass dedicated to repository format"""
204 207
205 208 type = deficiency
206 209 ### The following attributes should be defined for each class:
207 210
208 211 # machine-readable string uniquely identifying this improvement. it will be
209 212 # mapped to an action later in the upgrade process.
210 213 name = None
211 214
212 215 # message intended for humans explaining the improvement in more detail,
213 216 # including the implications of it ``deficiency`` types, should be worded
214 217 # in the present tense.
215 218 description = None
216 219
217 220 # message intended for humans explaining what an upgrade addressing this
218 221 # issue will do. should be worded in the future tense.
219 222 upgrademessage = None
220 223
221 224 # value of current Mercurial default for new repository
222 225 default = None
223 226
224 227 def __init__(self):
225 228 raise NotImplementedError()
226 229
227 230 @staticmethod
228 231 def fromrepo(repo):
229 232 """current value of the variant in the repository"""
230 233 raise NotImplementedError()
231 234
232 235 @staticmethod
233 236 def fromconfig(repo):
234 237 """current value of the variant in the configuration"""
235 238 raise NotImplementedError()
236 239
237 240
238 241 class requirementformatvariant(formatvariant):
239 242 """formatvariant based on a 'requirement' name.
240 243
241 244 Many format variant are controlled by a 'requirement'. We define a small
242 245 subclass to factor the code.
243 246 """
244 247
245 248 # the requirement that control this format variant
246 249 _requirement = None
247 250
248 251 @staticmethod
249 252 def _newreporequirements(ui):
250 253 return localrepo.newreporequirements(
251 254 ui, localrepo.defaultcreateopts(ui)
252 255 )
253 256
254 257 @classmethod
255 258 def fromrepo(cls, repo):
256 259 assert cls._requirement is not None
257 260 return cls._requirement in repo.requirements
258 261
259 262 @classmethod
260 263 def fromconfig(cls, repo):
261 264 assert cls._requirement is not None
262 265 return cls._requirement in cls._newreporequirements(repo.ui)
263 266
264 267
265 268 @registerformatvariant
266 269 class fncache(requirementformatvariant):
267 270 name = b'fncache'
268 271
269 272 _requirement = b'fncache'
270 273
271 274 default = True
272 275
273 276 description = _(
274 277 b'long and reserved filenames may not work correctly; '
275 278 b'repository performance is sub-optimal'
276 279 )
277 280
278 281 upgrademessage = _(
279 282 b'repository will be more resilient to storing '
280 283 b'certain paths and performance of certain '
281 284 b'operations should be improved'
282 285 )
283 286
284 287
285 288 @registerformatvariant
286 289 class dotencode(requirementformatvariant):
287 290 name = b'dotencode'
288 291
289 292 _requirement = b'dotencode'
290 293
291 294 default = True
292 295
293 296 description = _(
294 297 b'storage of filenames beginning with a period or '
295 298 b'space may not work correctly'
296 299 )
297 300
298 301 upgrademessage = _(
299 302 b'repository will be better able to store files '
300 303 b'beginning with a space or period'
301 304 )
302 305
303 306
304 307 @registerformatvariant
305 308 class generaldelta(requirementformatvariant):
306 309 name = b'generaldelta'
307 310
308 311 _requirement = b'generaldelta'
309 312
310 313 default = True
311 314
312 315 description = _(
313 316 b'deltas within internal storage are unable to '
314 317 b'choose optimal revisions; repository is larger and '
315 318 b'slower than it could be; interaction with other '
316 319 b'repositories may require extra network and CPU '
317 320 b'resources, making "hg push" and "hg pull" slower'
318 321 )
319 322
320 323 upgrademessage = _(
321 324 b'repository storage will be able to create '
322 325 b'optimal deltas; new repository data will be '
323 326 b'smaller and read times should decrease; '
324 327 b'interacting with other repositories using this '
325 328 b'storage model should require less network and '
326 329 b'CPU resources, making "hg push" and "hg pull" '
327 330 b'faster'
328 331 )
329 332
330 333
331 334 @registerformatvariant
332 335 class sparserevlog(requirementformatvariant):
333 336 name = b'sparserevlog'
334 337
335 338 _requirement = localrepo.SPARSEREVLOG_REQUIREMENT
336 339
337 340 default = True
338 341
339 342 description = _(
340 343 b'in order to limit disk reading and memory usage on older '
341 344 b'version, the span of a delta chain from its root to its '
342 345 b'end is limited, whatever the relevant data in this span. '
343 346 b'This can severly limit Mercurial ability to build good '
344 347 b'chain of delta resulting is much more storage space being '
345 348 b'taken and limit reusability of on disk delta during '
346 349 b'exchange.'
347 350 )
348 351
349 352 upgrademessage = _(
350 353 b'Revlog supports delta chain with more unused data '
351 354 b'between payload. These gaps will be skipped at read '
352 355 b'time. This allows for better delta chains, making a '
353 356 b'better compression and faster exchange with server.'
354 357 )
355 358
356 359
357 360 @registerformatvariant
358 361 class sidedata(requirementformatvariant):
359 362 name = b'sidedata'
360 363
361 364 _requirement = localrepo.SIDEDATA_REQUIREMENT
362 365
363 366 default = False
364 367
365 368 description = _(
366 369 b'Allows storage of extra data alongside a revision, '
367 370 b'unlocking various caching options.'
368 371 )
369 372
370 373 upgrademessage = _(b'Allows storage of extra data alongside a revision.')
371 374
372 375
373 376 @registerformatvariant
374 377 class copiessdc(requirementformatvariant):
375 378 name = b'copies-sdc'
376 379
377 380 _requirement = localrepo.COPIESSDC_REQUIREMENT
378 381
379 382 default = False
380 383
381 384 description = _(b'Stores copies information alongside changesets.')
382 385
383 386 upgrademessage = _(
384 387 b'Allows to use more efficient algorithm to deal with ' b'copy tracing.'
385 388 )
386 389
387 390
388 391 @registerformatvariant
389 392 class removecldeltachain(formatvariant):
390 393 name = b'plain-cl-delta'
391 394
392 395 default = True
393 396
394 397 description = _(
395 398 b'changelog storage is using deltas instead of '
396 399 b'raw entries; changelog reading and any '
397 400 b'operation relying on changelog data are slower '
398 401 b'than they could be'
399 402 )
400 403
401 404 upgrademessage = _(
402 405 b'changelog storage will be reformated to '
403 406 b'store raw entries; changelog reading will be '
404 407 b'faster; changelog size may be reduced'
405 408 )
406 409
407 410 @staticmethod
408 411 def fromrepo(repo):
409 412 # Mercurial 4.0 changed changelogs to not use delta chains. Search for
410 413 # changelogs with deltas.
411 414 cl = repo.changelog
412 415 chainbase = cl.chainbase
413 416 return all(rev == chainbase(rev) for rev in cl)
414 417
415 418 @staticmethod
416 419 def fromconfig(repo):
417 420 return True
418 421
419 422
420 423 @registerformatvariant
421 424 class compressionengine(formatvariant):
422 425 name = b'compression'
423 426 default = b'zlib'
424 427
425 428 description = _(
426 429 b'Compresion algorithm used to compress data. '
427 430 b'Some engine are faster than other'
428 431 )
429 432
430 433 upgrademessage = _(
431 434 b'revlog content will be recompressed with the new algorithm.'
432 435 )
433 436
434 437 @classmethod
435 438 def fromrepo(cls, repo):
436 439 # we allow multiple compression engine requirement to co-exist because
437 440 # strickly speaking, revlog seems to support mixed compression style.
438 441 #
439 442 # The compression used for new entries will be "the last one"
440 443 compression = b'zlib'
441 444 for req in repo.requirements:
442 445 prefix = req.startswith
443 446 if prefix(b'revlog-compression-') or prefix(b'exp-compression-'):
444 447 compression = req.split(b'-', 2)[2]
445 448 return compression
446 449
447 450 @classmethod
448 451 def fromconfig(cls, repo):
449 452 return repo.ui.config(b'format', b'revlog-compression')
450 453
451 454
452 455 @registerformatvariant
453 456 class compressionlevel(formatvariant):
454 457 name = b'compression-level'
455 458 default = b'default'
456 459
457 460 description = _(b'compression level')
458 461
459 462 upgrademessage = _(b'revlog content will be recompressed')
460 463
461 464 @classmethod
462 465 def fromrepo(cls, repo):
463 466 comp = compressionengine.fromrepo(repo)
464 467 level = None
465 468 if comp == b'zlib':
466 469 level = repo.ui.configint(b'storage', b'revlog.zlib.level')
467 470 elif comp == b'zstd':
468 471 level = repo.ui.configint(b'storage', b'revlog.zstd.level')
469 472 if level is None:
470 473 return b'default'
471 474 return bytes(level)
472 475
473 476 @classmethod
474 477 def fromconfig(cls, repo):
475 478 comp = compressionengine.fromconfig(repo)
476 479 level = None
477 480 if comp == b'zlib':
478 481 level = repo.ui.configint(b'storage', b'revlog.zlib.level')
479 482 elif comp == b'zstd':
480 483 level = repo.ui.configint(b'storage', b'revlog.zstd.level')
481 484 if level is None:
482 485 return b'default'
483 486 return bytes(level)
484 487
485 488
486 489 def finddeficiencies(repo):
487 490 """returns a list of deficiencies that the repo suffer from"""
488 491 deficiencies = []
489 492
490 493 # We could detect lack of revlogv1 and store here, but they were added
491 494 # in 0.9.2 and we don't support upgrading repos without these
492 495 # requirements, so let's not bother.
493 496
494 497 for fv in allformatvariant:
495 498 if not fv.fromrepo(repo):
496 499 deficiencies.append(fv)
497 500
498 501 return deficiencies
499 502
500 503
501 504 # search without '-' to support older form on newer client.
502 505 #
503 506 # We don't enforce backward compatibility for debug command so this
504 507 # might eventually be dropped. However, having to use two different
505 508 # forms in script when comparing result is anoying enough to add
506 509 # backward compatibility for a while.
507 510 legacy_opts_map = {
508 511 b'redeltaparent': b're-delta-parent',
509 512 b'redeltamultibase': b're-delta-multibase',
510 513 b'redeltaall': b're-delta-all',
511 514 b'redeltafulladd': b're-delta-fulladd',
512 515 }
513 516
514 517
515 518 def findoptimizations(repo):
516 519 """Determine optimisation that could be used during upgrade"""
517 520 # These are unconditionally added. There is logic later that figures out
518 521 # which ones to apply.
519 522 optimizations = []
520 523
521 524 optimizations.append(
522 525 improvement(
523 526 name=b're-delta-parent',
524 527 type=optimisation,
525 528 description=_(
526 529 b'deltas within internal storage will be recalculated to '
527 530 b'choose an optimal base revision where this was not '
528 531 b'already done; the size of the repository may shrink and '
529 532 b'various operations may become faster; the first time '
530 533 b'this optimization is performed could slow down upgrade '
531 534 b'execution considerably; subsequent invocations should '
532 535 b'not run noticeably slower'
533 536 ),
534 537 upgrademessage=_(
535 538 b'deltas within internal storage will choose a new '
536 539 b'base revision if needed'
537 540 ),
538 541 )
539 542 )
540 543
541 544 optimizations.append(
542 545 improvement(
543 546 name=b're-delta-multibase',
544 547 type=optimisation,
545 548 description=_(
546 549 b'deltas within internal storage will be recalculated '
547 550 b'against multiple base revision and the smallest '
548 551 b'difference will be used; the size of the repository may '
549 552 b'shrink significantly when there are many merges; this '
550 553 b'optimization will slow down execution in proportion to '
551 554 b'the number of merges in the repository and the amount '
552 555 b'of files in the repository; this slow down should not '
553 556 b'be significant unless there are tens of thousands of '
554 557 b'files and thousands of merges'
555 558 ),
556 559 upgrademessage=_(
557 560 b'deltas within internal storage will choose an '
558 561 b'optimal delta by computing deltas against multiple '
559 562 b'parents; may slow down execution time '
560 563 b'significantly'
561 564 ),
562 565 )
563 566 )
564 567
565 568 optimizations.append(
566 569 improvement(
567 570 name=b're-delta-all',
568 571 type=optimisation,
569 572 description=_(
570 573 b'deltas within internal storage will always be '
571 574 b'recalculated without reusing prior deltas; this will '
572 575 b'likely make execution run several times slower; this '
573 576 b'optimization is typically not needed'
574 577 ),
575 578 upgrademessage=_(
576 579 b'deltas within internal storage will be fully '
577 580 b'recomputed; this will likely drastically slow down '
578 581 b'execution time'
579 582 ),
580 583 )
581 584 )
582 585
583 586 optimizations.append(
584 587 improvement(
585 588 name=b're-delta-fulladd',
586 589 type=optimisation,
587 590 description=_(
588 591 b'every revision will be re-added as if it was new '
589 592 b'content. It will go through the full storage '
590 593 b'mechanism giving extensions a chance to process it '
591 594 b'(eg. lfs). This is similar to "re-delta-all" but even '
592 595 b'slower since more logic is involved.'
593 596 ),
594 597 upgrademessage=_(
595 598 b'each revision will be added as new content to the '
596 599 b'internal storage; this will likely drastically slow '
597 600 b'down execution time, but some extensions might need '
598 601 b'it'
599 602 ),
600 603 )
601 604 )
602 605
603 606 return optimizations
604 607
605 608
606 609 def determineactions(repo, deficiencies, sourcereqs, destreqs):
607 610 """Determine upgrade actions that will be performed.
608 611
609 612 Given a list of improvements as returned by ``finddeficiencies`` and
610 613 ``findoptimizations``, determine the list of upgrade actions that
611 614 will be performed.
612 615
613 616 The role of this function is to filter improvements if needed, apply
614 617 recommended optimizations from the improvements list that make sense,
615 618 etc.
616 619
617 620 Returns a list of action names.
618 621 """
619 622 newactions = []
620 623
621 624 knownreqs = supporteddestrequirements(repo)
622 625
623 626 for d in deficiencies:
624 627 name = d.name
625 628
626 629 # If the action is a requirement that doesn't show up in the
627 630 # destination requirements, prune the action.
628 631 if name in knownreqs and name not in destreqs:
629 632 continue
630 633
631 634 newactions.append(d)
632 635
633 636 # FUTURE consider adding some optimizations here for certain transitions.
634 637 # e.g. adding generaldelta could schedule parent redeltas.
635 638
636 639 return newactions
637 640
638 641
639 642 def _revlogfrompath(repo, path):
640 643 """Obtain a revlog from a repo path.
641 644
642 645 An instance of the appropriate class is returned.
643 646 """
644 647 if path == b'00changelog.i':
645 648 return changelog.changelog(repo.svfs)
646 649 elif path.endswith(b'00manifest.i'):
647 650 mandir = path[: -len(b'00manifest.i')]
648 651 return manifest.manifestrevlog(repo.svfs, tree=mandir)
649 652 else:
650 653 # reverse of "/".join(("data", path + ".i"))
651 654 return filelog.filelog(repo.svfs, path[5:-2])
652 655
653 656
654 657 def _copyrevlog(tr, destrepo, oldrl, unencodedname):
655 658 """copy all relevant files for `oldrl` into `destrepo` store
656 659
657 660 Files are copied "as is" without any transformation. The copy is performed
658 661 without extra checks. Callers are responsible for making sure the copied
659 662 content is compatible with format of the destination repository.
660 663 """
661 664 oldrl = getattr(oldrl, '_revlog', oldrl)
662 665 newrl = _revlogfrompath(destrepo, unencodedname)
663 666 newrl = getattr(newrl, '_revlog', newrl)
664 667
665 668 oldvfs = oldrl.opener
666 669 newvfs = newrl.opener
667 670 oldindex = oldvfs.join(oldrl.indexfile)
668 671 newindex = newvfs.join(newrl.indexfile)
669 672 olddata = oldvfs.join(oldrl.datafile)
670 673 newdata = newvfs.join(newrl.datafile)
671 674
672 675 with newvfs(newrl.indexfile, b'w'):
673 676 pass # create all the directories
674 677
675 678 util.copyfile(oldindex, newindex)
676 679 copydata = oldrl.opener.exists(oldrl.datafile)
677 680 if copydata:
678 681 util.copyfile(olddata, newdata)
679 682
680 683 if not (
681 684 unencodedname.endswith(b'00changelog.i')
682 685 or unencodedname.endswith(b'00manifest.i')
683 686 ):
684 687 destrepo.svfs.fncache.add(unencodedname)
685 688 if copydata:
686 689 destrepo.svfs.fncache.add(unencodedname[:-2] + b'.d')
687 690
688 691
689 692 UPGRADE_CHANGELOG = object()
690 693 UPGRADE_MANIFEST = object()
691 694 UPGRADE_FILELOG = object()
692 695
693 696 UPGRADE_ALL_REVLOGS = frozenset(
694 697 [UPGRADE_CHANGELOG, UPGRADE_MANIFEST, UPGRADE_FILELOG]
695 698 )
696 699
697 700
698 701 def getsidedatacompanion(srcrepo, dstrepo):
699 702 sidedatacompanion = None
700 703 removedreqs = srcrepo.requirements - dstrepo.requirements
704 addedreqs = dstrepo.requirements - srcrepo.requirements
701 705 if localrepo.SIDEDATA_REQUIREMENT in removedreqs:
702 706
703 707 def sidedatacompanion(rl, rev):
704 708 rl = getattr(rl, '_revlog', rl)
705 709 if rl.flags(rev) & revlog.REVIDX_SIDEDATA:
706 710 return True, (), {}
707 711 return False, (), {}
708 712
713 elif localrepo.COPIESSDC_REQUIREMENT in addedreqs:
714 sidedatacompanion = copies.getsidedataadder(srcrepo, dstrepo)
715 elif localrepo.COPIESSDC_REQUIREMENT in removedreqs:
716 sidedatacompanion = copies.getsidedataremover(srcrepo, dstrepo)
709 717 return sidedatacompanion
710 718
711 719
712 720 def matchrevlog(revlogfilter, entry):
713 721 """check is a revlog is selected for cloning
714 722
715 723 The store entry is checked against the passed filter"""
716 724 if entry.endswith(b'00changelog.i'):
717 725 return UPGRADE_CHANGELOG in revlogfilter
718 726 elif entry.endswith(b'00manifest.i'):
719 727 return UPGRADE_MANIFEST in revlogfilter
720 728 return UPGRADE_FILELOG in revlogfilter
721 729
722 730
723 731 def _clonerevlogs(
724 732 ui,
725 733 srcrepo,
726 734 dstrepo,
727 735 tr,
728 736 deltareuse,
729 737 forcedeltabothparents,
730 738 revlogs=UPGRADE_ALL_REVLOGS,
731 739 ):
732 740 """Copy revlogs between 2 repos."""
733 741 revcount = 0
734 742 srcsize = 0
735 743 srcrawsize = 0
736 744 dstsize = 0
737 745 fcount = 0
738 746 frevcount = 0
739 747 fsrcsize = 0
740 748 frawsize = 0
741 749 fdstsize = 0
742 750 mcount = 0
743 751 mrevcount = 0
744 752 msrcsize = 0
745 753 mrawsize = 0
746 754 mdstsize = 0
747 755 crevcount = 0
748 756 csrcsize = 0
749 757 crawsize = 0
750 758 cdstsize = 0
751 759
752 760 alldatafiles = list(srcrepo.store.walk())
753 761
754 762 # Perform a pass to collect metadata. This validates we can open all
755 763 # source files and allows a unified progress bar to be displayed.
756 764 for unencoded, encoded, size in alldatafiles:
757 765 if unencoded.endswith(b'.d'):
758 766 continue
759 767
760 768 rl = _revlogfrompath(srcrepo, unencoded)
761 769
762 770 info = rl.storageinfo(
763 771 exclusivefiles=True,
764 772 revisionscount=True,
765 773 trackedsize=True,
766 774 storedsize=True,
767 775 )
768 776
769 777 revcount += info[b'revisionscount'] or 0
770 778 datasize = info[b'storedsize'] or 0
771 779 rawsize = info[b'trackedsize'] or 0
772 780
773 781 srcsize += datasize
774 782 srcrawsize += rawsize
775 783
776 784 # This is for the separate progress bars.
777 785 if isinstance(rl, changelog.changelog):
778 786 crevcount += len(rl)
779 787 csrcsize += datasize
780 788 crawsize += rawsize
781 789 elif isinstance(rl, manifest.manifestrevlog):
782 790 mcount += 1
783 791 mrevcount += len(rl)
784 792 msrcsize += datasize
785 793 mrawsize += rawsize
786 794 elif isinstance(rl, filelog.filelog):
787 795 fcount += 1
788 796 frevcount += len(rl)
789 797 fsrcsize += datasize
790 798 frawsize += rawsize
791 799 else:
792 800 error.ProgrammingError(b'unknown revlog type')
793 801
794 802 if not revcount:
795 803 return
796 804
797 805 ui.write(
798 806 _(
799 807 b'migrating %d total revisions (%d in filelogs, %d in manifests, '
800 808 b'%d in changelog)\n'
801 809 )
802 810 % (revcount, frevcount, mrevcount, crevcount)
803 811 )
804 812 ui.write(
805 813 _(b'migrating %s in store; %s tracked data\n')
806 814 % ((util.bytecount(srcsize), util.bytecount(srcrawsize)))
807 815 )
808 816
809 817 # Used to keep track of progress.
810 818 progress = None
811 819
812 820 def oncopiedrevision(rl, rev, node):
813 821 progress.increment()
814 822
815 823 sidedatacompanion = getsidedatacompanion(srcrepo, dstrepo)
816 824
817 825 # Do the actual copying.
818 826 # FUTURE this operation can be farmed off to worker processes.
819 827 seen = set()
820 828 for unencoded, encoded, size in alldatafiles:
821 829 if unencoded.endswith(b'.d'):
822 830 continue
823 831
824 832 oldrl = _revlogfrompath(srcrepo, unencoded)
825 833
826 834 if isinstance(oldrl, changelog.changelog) and b'c' not in seen:
827 835 ui.write(
828 836 _(
829 837 b'finished migrating %d manifest revisions across %d '
830 838 b'manifests; change in size: %s\n'
831 839 )
832 840 % (mrevcount, mcount, util.bytecount(mdstsize - msrcsize))
833 841 )
834 842
835 843 ui.write(
836 844 _(
837 845 b'migrating changelog containing %d revisions '
838 846 b'(%s in store; %s tracked data)\n'
839 847 )
840 848 % (
841 849 crevcount,
842 850 util.bytecount(csrcsize),
843 851 util.bytecount(crawsize),
844 852 )
845 853 )
846 854 seen.add(b'c')
847 855 progress = srcrepo.ui.makeprogress(
848 856 _(b'changelog revisions'), total=crevcount
849 857 )
850 858 elif isinstance(oldrl, manifest.manifestrevlog) and b'm' not in seen:
851 859 ui.write(
852 860 _(
853 861 b'finished migrating %d filelog revisions across %d '
854 862 b'filelogs; change in size: %s\n'
855 863 )
856 864 % (frevcount, fcount, util.bytecount(fdstsize - fsrcsize))
857 865 )
858 866
859 867 ui.write(
860 868 _(
861 869 b'migrating %d manifests containing %d revisions '
862 870 b'(%s in store; %s tracked data)\n'
863 871 )
864 872 % (
865 873 mcount,
866 874 mrevcount,
867 875 util.bytecount(msrcsize),
868 876 util.bytecount(mrawsize),
869 877 )
870 878 )
871 879 seen.add(b'm')
872 880 if progress:
873 881 progress.complete()
874 882 progress = srcrepo.ui.makeprogress(
875 883 _(b'manifest revisions'), total=mrevcount
876 884 )
877 885 elif b'f' not in seen:
878 886 ui.write(
879 887 _(
880 888 b'migrating %d filelogs containing %d revisions '
881 889 b'(%s in store; %s tracked data)\n'
882 890 )
883 891 % (
884 892 fcount,
885 893 frevcount,
886 894 util.bytecount(fsrcsize),
887 895 util.bytecount(frawsize),
888 896 )
889 897 )
890 898 seen.add(b'f')
891 899 if progress:
892 900 progress.complete()
893 901 progress = srcrepo.ui.makeprogress(
894 902 _(b'file revisions'), total=frevcount
895 903 )
896 904
897 905 if matchrevlog(revlogs, unencoded):
898 906 ui.note(
899 907 _(b'cloning %d revisions from %s\n') % (len(oldrl), unencoded)
900 908 )
901 909 newrl = _revlogfrompath(dstrepo, unencoded)
902 910 oldrl.clone(
903 911 tr,
904 912 newrl,
905 913 addrevisioncb=oncopiedrevision,
906 914 deltareuse=deltareuse,
907 915 forcedeltabothparents=forcedeltabothparents,
908 916 sidedatacompanion=sidedatacompanion,
909 917 )
910 918 else:
911 919 msg = _(b'blindly copying %s containing %i revisions\n')
912 920 ui.note(msg % (unencoded, len(oldrl)))
913 921 _copyrevlog(tr, dstrepo, oldrl, unencoded)
914 922
915 923 newrl = _revlogfrompath(dstrepo, unencoded)
916 924
917 925 info = newrl.storageinfo(storedsize=True)
918 926 datasize = info[b'storedsize'] or 0
919 927
920 928 dstsize += datasize
921 929
922 930 if isinstance(newrl, changelog.changelog):
923 931 cdstsize += datasize
924 932 elif isinstance(newrl, manifest.manifestrevlog):
925 933 mdstsize += datasize
926 934 else:
927 935 fdstsize += datasize
928 936
929 937 progress.complete()
930 938
931 939 ui.write(
932 940 _(
933 941 b'finished migrating %d changelog revisions; change in size: '
934 942 b'%s\n'
935 943 )
936 944 % (crevcount, util.bytecount(cdstsize - csrcsize))
937 945 )
938 946
939 947 ui.write(
940 948 _(
941 949 b'finished migrating %d total revisions; total change in store '
942 950 b'size: %s\n'
943 951 )
944 952 % (revcount, util.bytecount(dstsize - srcsize))
945 953 )
946 954
947 955
948 956 def _filterstorefile(srcrepo, dstrepo, requirements, path, mode, st):
949 957 """Determine whether to copy a store file during upgrade.
950 958
951 959 This function is called when migrating store files from ``srcrepo`` to
952 960 ``dstrepo`` as part of upgrading a repository.
953 961
954 962 Args:
955 963 srcrepo: repo we are copying from
956 964 dstrepo: repo we are copying to
957 965 requirements: set of requirements for ``dstrepo``
958 966 path: store file being examined
959 967 mode: the ``ST_MODE`` file type of ``path``
960 968 st: ``stat`` data structure for ``path``
961 969
962 970 Function should return ``True`` if the file is to be copied.
963 971 """
964 972 # Skip revlogs.
965 973 if path.endswith((b'.i', b'.d')):
966 974 return False
967 975 # Skip transaction related files.
968 976 if path.startswith(b'undo'):
969 977 return False
970 978 # Only copy regular files.
971 979 if mode != stat.S_IFREG:
972 980 return False
973 981 # Skip other skipped files.
974 982 if path in (b'lock', b'fncache'):
975 983 return False
976 984
977 985 return True
978 986
979 987
980 988 def _finishdatamigration(ui, srcrepo, dstrepo, requirements):
981 989 """Hook point for extensions to perform additional actions during upgrade.
982 990
983 991 This function is called after revlogs and store files have been copied but
984 992 before the new store is swapped into the original location.
985 993 """
986 994
987 995
988 996 def _upgraderepo(
989 997 ui, srcrepo, dstrepo, requirements, actions, revlogs=UPGRADE_ALL_REVLOGS
990 998 ):
991 999 """Do the low-level work of upgrading a repository.
992 1000
993 1001 The upgrade is effectively performed as a copy between a source
994 1002 repository and a temporary destination repository.
995 1003
996 1004 The source repository is unmodified for as long as possible so the
997 1005 upgrade can abort at any time without causing loss of service for
998 1006 readers and without corrupting the source repository.
999 1007 """
1000 1008 assert srcrepo.currentwlock()
1001 1009 assert dstrepo.currentwlock()
1002 1010
1003 1011 ui.write(
1004 1012 _(
1005 1013 b'(it is safe to interrupt this process any time before '
1006 1014 b'data migration completes)\n'
1007 1015 )
1008 1016 )
1009 1017
1010 1018 if b're-delta-all' in actions:
1011 1019 deltareuse = revlog.revlog.DELTAREUSENEVER
1012 1020 elif b're-delta-parent' in actions:
1013 1021 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
1014 1022 elif b're-delta-multibase' in actions:
1015 1023 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
1016 1024 elif b're-delta-fulladd' in actions:
1017 1025 deltareuse = revlog.revlog.DELTAREUSEFULLADD
1018 1026 else:
1019 1027 deltareuse = revlog.revlog.DELTAREUSEALWAYS
1020 1028
1021 1029 with dstrepo.transaction(b'upgrade') as tr:
1022 1030 _clonerevlogs(
1023 1031 ui,
1024 1032 srcrepo,
1025 1033 dstrepo,
1026 1034 tr,
1027 1035 deltareuse,
1028 1036 b're-delta-multibase' in actions,
1029 1037 revlogs=revlogs,
1030 1038 )
1031 1039
1032 1040 # Now copy other files in the store directory.
1033 1041 # The sorted() makes execution deterministic.
1034 1042 for p, kind, st in sorted(srcrepo.store.vfs.readdir(b'', stat=True)):
1035 1043 if not _filterstorefile(srcrepo, dstrepo, requirements, p, kind, st):
1036 1044 continue
1037 1045
1038 1046 srcrepo.ui.write(_(b'copying %s\n') % p)
1039 1047 src = srcrepo.store.rawvfs.join(p)
1040 1048 dst = dstrepo.store.rawvfs.join(p)
1041 1049 util.copyfile(src, dst, copystat=True)
1042 1050
1043 1051 _finishdatamigration(ui, srcrepo, dstrepo, requirements)
1044 1052
1045 1053 ui.write(_(b'data fully migrated to temporary repository\n'))
1046 1054
1047 1055 backuppath = pycompat.mkdtemp(prefix=b'upgradebackup.', dir=srcrepo.path)
1048 1056 backupvfs = vfsmod.vfs(backuppath)
1049 1057
1050 1058 # Make a backup of requires file first, as it is the first to be modified.
1051 1059 util.copyfile(srcrepo.vfs.join(b'requires'), backupvfs.join(b'requires'))
1052 1060
1053 1061 # We install an arbitrary requirement that clients must not support
1054 1062 # as a mechanism to lock out new clients during the data swap. This is
1055 1063 # better than allowing a client to continue while the repository is in
1056 1064 # an inconsistent state.
1057 1065 ui.write(
1058 1066 _(
1059 1067 b'marking source repository as being upgraded; clients will be '
1060 1068 b'unable to read from repository\n'
1061 1069 )
1062 1070 )
1063 1071 scmutil.writerequires(
1064 1072 srcrepo.vfs, srcrepo.requirements | {b'upgradeinprogress'}
1065 1073 )
1066 1074
1067 1075 ui.write(_(b'starting in-place swap of repository data\n'))
1068 1076 ui.write(_(b'replaced files will be backed up at %s\n') % backuppath)
1069 1077
1070 1078 # Now swap in the new store directory. Doing it as a rename should make
1071 1079 # the operation nearly instantaneous and atomic (at least in well-behaved
1072 1080 # environments).
1073 1081 ui.write(_(b'replacing store...\n'))
1074 1082 tstart = util.timer()
1075 1083 util.rename(srcrepo.spath, backupvfs.join(b'store'))
1076 1084 util.rename(dstrepo.spath, srcrepo.spath)
1077 1085 elapsed = util.timer() - tstart
1078 1086 ui.write(
1079 1087 _(
1080 1088 b'store replacement complete; repository was inconsistent for '
1081 1089 b'%0.1fs\n'
1082 1090 )
1083 1091 % elapsed
1084 1092 )
1085 1093
1086 1094 # We first write the requirements file. Any new requirements will lock
1087 1095 # out legacy clients.
1088 1096 ui.write(
1089 1097 _(
1090 1098 b'finalizing requirements file and making repository readable '
1091 1099 b'again\n'
1092 1100 )
1093 1101 )
1094 1102 scmutil.writerequires(srcrepo.vfs, requirements)
1095 1103
1096 1104 # The lock file from the old store won't be removed because nothing has a
1097 1105 # reference to its new location. So clean it up manually. Alternatively, we
1098 1106 # could update srcrepo.svfs and other variables to point to the new
1099 1107 # location. This is simpler.
1100 1108 backupvfs.unlink(b'store/lock')
1101 1109
1102 1110 return backuppath
1103 1111
1104 1112
1105 1113 def upgraderepo(
1106 1114 ui,
1107 1115 repo,
1108 1116 run=False,
1109 1117 optimize=None,
1110 1118 backup=True,
1111 1119 manifest=None,
1112 1120 changelog=None,
1113 1121 ):
1114 1122 """Upgrade a repository in place."""
1115 1123 if optimize is None:
1116 1124 optimize = []
1117 1125 optimize = set(legacy_opts_map.get(o, o) for o in optimize)
1118 1126 repo = repo.unfiltered()
1119 1127
1120 1128 revlogs = set(UPGRADE_ALL_REVLOGS)
1121 1129 specentries = ((b'c', changelog), (b'm', manifest))
1122 1130 specified = [(y, x) for (y, x) in specentries if x is not None]
1123 1131 if specified:
1124 1132 # we have some limitation on revlogs to be recloned
1125 1133 if any(x for y, x in specified):
1126 1134 revlogs = set()
1127 1135 for r, enabled in specified:
1128 1136 if enabled:
1129 1137 if r == b'c':
1130 1138 revlogs.add(UPGRADE_CHANGELOG)
1131 1139 elif r == b'm':
1132 1140 revlogs.add(UPGRADE_MANIFEST)
1133 1141 else:
1134 1142 # none are enabled
1135 1143 for r, __ in specified:
1136 1144 if r == b'c':
1137 1145 revlogs.discard(UPGRADE_CHANGELOG)
1138 1146 elif r == b'm':
1139 1147 revlogs.discard(UPGRADE_MANIFEST)
1140 1148
1141 1149 # Ensure the repository can be upgraded.
1142 1150 missingreqs = requiredsourcerequirements(repo) - repo.requirements
1143 1151 if missingreqs:
1144 1152 raise error.Abort(
1145 1153 _(b'cannot upgrade repository; requirement missing: %s')
1146 1154 % _(b', ').join(sorted(missingreqs))
1147 1155 )
1148 1156
1149 1157 blockedreqs = blocksourcerequirements(repo) & repo.requirements
1150 1158 if blockedreqs:
1151 1159 raise error.Abort(
1152 1160 _(
1153 1161 b'cannot upgrade repository; unsupported source '
1154 1162 b'requirement: %s'
1155 1163 )
1156 1164 % _(b', ').join(sorted(blockedreqs))
1157 1165 )
1158 1166
1159 1167 # FUTURE there is potentially a need to control the wanted requirements via
1160 1168 # command arguments or via an extension hook point.
1161 1169 newreqs = localrepo.newreporequirements(
1162 1170 repo.ui, localrepo.defaultcreateopts(repo.ui)
1163 1171 )
1164 1172 newreqs.update(preservedrequirements(repo))
1165 1173
1166 1174 noremovereqs = (
1167 1175 repo.requirements - newreqs - supportremovedrequirements(repo)
1168 1176 )
1169 1177 if noremovereqs:
1170 1178 raise error.Abort(
1171 1179 _(
1172 1180 b'cannot upgrade repository; requirement would be '
1173 1181 b'removed: %s'
1174 1182 )
1175 1183 % _(b', ').join(sorted(noremovereqs))
1176 1184 )
1177 1185
1178 1186 noaddreqs = newreqs - repo.requirements - allowednewrequirements(repo)
1179 1187 if noaddreqs:
1180 1188 raise error.Abort(
1181 1189 _(
1182 1190 b'cannot upgrade repository; do not support adding '
1183 1191 b'requirement: %s'
1184 1192 )
1185 1193 % _(b', ').join(sorted(noaddreqs))
1186 1194 )
1187 1195
1188 1196 unsupportedreqs = newreqs - supporteddestrequirements(repo)
1189 1197 if unsupportedreqs:
1190 1198 raise error.Abort(
1191 1199 _(
1192 1200 b'cannot upgrade repository; do not support '
1193 1201 b'destination requirement: %s'
1194 1202 )
1195 1203 % _(b', ').join(sorted(unsupportedreqs))
1196 1204 )
1197 1205
1198 1206 # Find and validate all improvements that can be made.
1199 1207 alloptimizations = findoptimizations(repo)
1200 1208
1201 1209 # Apply and Validate arguments.
1202 1210 optimizations = []
1203 1211 for o in alloptimizations:
1204 1212 if o.name in optimize:
1205 1213 optimizations.append(o)
1206 1214 optimize.discard(o.name)
1207 1215
1208 1216 if optimize: # anything left is unknown
1209 1217 raise error.Abort(
1210 1218 _(b'unknown optimization action requested: %s')
1211 1219 % b', '.join(sorted(optimize)),
1212 1220 hint=_(b'run without arguments to see valid optimizations'),
1213 1221 )
1214 1222
1215 1223 deficiencies = finddeficiencies(repo)
1216 1224 actions = determineactions(repo, deficiencies, repo.requirements, newreqs)
1217 1225 actions.extend(
1218 1226 o
1219 1227 for o in sorted(optimizations)
1220 1228 # determineactions could have added optimisation
1221 1229 if o not in actions
1222 1230 )
1223 1231
1224 1232 removedreqs = repo.requirements - newreqs
1225 1233 addedreqs = newreqs - repo.requirements
1226 1234
1227 1235 if revlogs != UPGRADE_ALL_REVLOGS:
1228 1236 incompatible = RECLONES_REQUIREMENTS & (removedreqs | addedreqs)
1229 1237 if incompatible:
1230 1238 msg = _(
1231 1239 b'ignoring revlogs selection flags, format requirements '
1232 1240 b'change: %s\n'
1233 1241 )
1234 1242 ui.warn(msg % b', '.join(sorted(incompatible)))
1235 1243 revlogs = UPGRADE_ALL_REVLOGS
1236 1244
1237 1245 def printrequirements():
1238 1246 ui.write(_(b'requirements\n'))
1239 1247 ui.write(
1240 1248 _(b' preserved: %s\n')
1241 1249 % _(b', ').join(sorted(newreqs & repo.requirements))
1242 1250 )
1243 1251
1244 1252 if repo.requirements - newreqs:
1245 1253 ui.write(
1246 1254 _(b' removed: %s\n')
1247 1255 % _(b', ').join(sorted(repo.requirements - newreqs))
1248 1256 )
1249 1257
1250 1258 if newreqs - repo.requirements:
1251 1259 ui.write(
1252 1260 _(b' added: %s\n')
1253 1261 % _(b', ').join(sorted(newreqs - repo.requirements))
1254 1262 )
1255 1263
1256 1264 ui.write(b'\n')
1257 1265
1258 1266 def printupgradeactions():
1259 1267 for a in actions:
1260 1268 ui.write(b'%s\n %s\n\n' % (a.name, a.upgrademessage))
1261 1269
1262 1270 if not run:
1263 1271 fromconfig = []
1264 1272 onlydefault = []
1265 1273
1266 1274 for d in deficiencies:
1267 1275 if d.fromconfig(repo):
1268 1276 fromconfig.append(d)
1269 1277 elif d.default:
1270 1278 onlydefault.append(d)
1271 1279
1272 1280 if fromconfig or onlydefault:
1273 1281
1274 1282 if fromconfig:
1275 1283 ui.write(
1276 1284 _(
1277 1285 b'repository lacks features recommended by '
1278 1286 b'current config options:\n\n'
1279 1287 )
1280 1288 )
1281 1289 for i in fromconfig:
1282 1290 ui.write(b'%s\n %s\n\n' % (i.name, i.description))
1283 1291
1284 1292 if onlydefault:
1285 1293 ui.write(
1286 1294 _(
1287 1295 b'repository lacks features used by the default '
1288 1296 b'config options:\n\n'
1289 1297 )
1290 1298 )
1291 1299 for i in onlydefault:
1292 1300 ui.write(b'%s\n %s\n\n' % (i.name, i.description))
1293 1301
1294 1302 ui.write(b'\n')
1295 1303 else:
1296 1304 ui.write(
1297 1305 _(
1298 1306 b'(no feature deficiencies found in existing '
1299 1307 b'repository)\n'
1300 1308 )
1301 1309 )
1302 1310
1303 1311 ui.write(
1304 1312 _(
1305 1313 b'performing an upgrade with "--run" will make the following '
1306 1314 b'changes:\n\n'
1307 1315 )
1308 1316 )
1309 1317
1310 1318 printrequirements()
1311 1319 printupgradeactions()
1312 1320
1313 1321 unusedoptimize = [i for i in alloptimizations if i not in actions]
1314 1322
1315 1323 if unusedoptimize:
1316 1324 ui.write(
1317 1325 _(
1318 1326 b'additional optimizations are available by specifying '
1319 1327 b'"--optimize <name>":\n\n'
1320 1328 )
1321 1329 )
1322 1330 for i in unusedoptimize:
1323 1331 ui.write(_(b'%s\n %s\n\n') % (i.name, i.description))
1324 1332 return
1325 1333
1326 1334 # Else we're in the run=true case.
1327 1335 ui.write(_(b'upgrade will perform the following actions:\n\n'))
1328 1336 printrequirements()
1329 1337 printupgradeactions()
1330 1338
1331 1339 upgradeactions = [a.name for a in actions]
1332 1340
1333 1341 ui.write(_(b'beginning upgrade...\n'))
1334 1342 with repo.wlock(), repo.lock():
1335 1343 ui.write(_(b'repository locked and read-only\n'))
1336 1344 # Our strategy for upgrading the repository is to create a new,
1337 1345 # temporary repository, write data to it, then do a swap of the
1338 1346 # data. There are less heavyweight ways to do this, but it is easier
1339 1347 # to create a new repo object than to instantiate all the components
1340 1348 # (like the store) separately.
1341 1349 tmppath = pycompat.mkdtemp(prefix=b'upgrade.', dir=repo.path)
1342 1350 backuppath = None
1343 1351 try:
1344 1352 ui.write(
1345 1353 _(
1346 1354 b'creating temporary repository to stage migrated '
1347 1355 b'data: %s\n'
1348 1356 )
1349 1357 % tmppath
1350 1358 )
1351 1359
1352 1360 # clone ui without using ui.copy because repo.ui is protected
1353 1361 repoui = repo.ui.__class__(repo.ui)
1354 1362 dstrepo = hg.repository(repoui, path=tmppath, create=True)
1355 1363
1356 1364 with dstrepo.wlock(), dstrepo.lock():
1357 1365 backuppath = _upgraderepo(
1358 1366 ui, repo, dstrepo, newreqs, upgradeactions, revlogs=revlogs
1359 1367 )
1360 1368 if not (backup or backuppath is None):
1361 1369 ui.write(_(b'removing old repository content%s\n') % backuppath)
1362 1370 repo.vfs.rmtree(backuppath, forcibly=True)
1363 1371 backuppath = None
1364 1372
1365 1373 finally:
1366 1374 ui.write(_(b'removing temporary repository %s\n') % tmppath)
1367 1375 repo.vfs.rmtree(tmppath, forcibly=True)
1368 1376
1369 1377 if backuppath:
1370 1378 ui.warn(
1371 1379 _(b'copy of old repository backed up at %s\n') % backuppath
1372 1380 )
1373 1381 ui.warn(
1374 1382 _(
1375 1383 b'the old repository will not be deleted; remove '
1376 1384 b'it to free up disk space once the upgraded '
1377 1385 b'repository is verified\n'
1378 1386 )
1379 1387 )
@@ -1,453 +1,540 b''
1 1 #testcases extra sidedata
2 2
3 3 #if extra
4 4 $ cat >> $HGRCPATH << EOF
5 5 > [experimental]
6 6 > copies.write-to=changeset-only
7 7 > copies.read-from=changeset-only
8 8 > [alias]
9 9 > changesetcopies = log -r . -T 'files: {files}
10 10 > {extras % "{ifcontains("files", key, "{key}: {value}\n")}"}
11 11 > {extras % "{ifcontains("copies", key, "{key}: {value}\n")}"}'
12 12 > EOF
13 13 #endif
14 14
15 15 #if sidedata
16 16 $ cat >> $HGRCPATH << EOF
17 17 > [format]
18 18 > exp-use-copies-side-data-changeset = yes
19 19 > EOF
20 20 #endif
21 21
22 22 $ cat >> $HGRCPATH << EOF
23 23 > [alias]
24 24 > showcopies = log -r . -T '{file_copies % "{source} -> {name}\n"}'
25 25 > [extensions]
26 26 > rebase =
27 27 > split =
28 28 > EOF
29 29
30 30 Check that copies are recorded correctly
31 31
32 32 $ hg init repo
33 33 $ cd repo
34 34 #if sidedata
35 35 $ hg debugformat -v
36 36 format-variant repo config default
37 37 fncache: yes yes yes
38 38 dotencode: yes yes yes
39 39 generaldelta: yes yes yes
40 40 sparserevlog: yes yes yes
41 41 sidedata: yes yes no
42 42 copies-sdc: yes yes no
43 43 plain-cl-delta: yes yes yes
44 44 compression: zlib zlib zlib
45 45 compression-level: default default default
46 46 #else
47 47 $ hg debugformat -v
48 48 format-variant repo config default
49 49 fncache: yes yes yes
50 50 dotencode: yes yes yes
51 51 generaldelta: yes yes yes
52 52 sparserevlog: yes yes yes
53 53 sidedata: no no no
54 54 copies-sdc: no no no
55 55 plain-cl-delta: yes yes yes
56 56 compression: zlib zlib zlib
57 57 compression-level: default default default
58 58 #endif
59 59 $ echo a > a
60 60 $ hg add a
61 61 $ hg ci -m initial
62 62 $ hg cp a b
63 63 $ hg cp a c
64 64 $ hg cp a d
65 65 $ hg ci -m 'copy a to b, c, and d'
66 66
67 67 #if extra
68 68
69 69 $ hg changesetcopies
70 70 files: b c d
71 71 filesadded: 0
72 72 1
73 73 2
74 74
75 75 p1copies: 0\x00a (esc)
76 76 1\x00a (esc)
77 77 2\x00a (esc)
78 78 #else
79 79 $ hg debugsidedata -c -v -- -1
80 80 4 sidedata entries
81 81 entry-0010 size 11
82 82 '0\x00a\n1\x00a\n2\x00a'
83 83 entry-0011 size 0
84 84 ''
85 85 entry-0012 size 5
86 86 '0\n1\n2'
87 87 entry-0013 size 0
88 88 ''
89 89 #endif
90 90
91 91 $ hg showcopies
92 92 a -> b
93 93 a -> c
94 94 a -> d
95 95
96 96 #if extra
97 97
98 98 $ hg showcopies --config experimental.copies.read-from=compatibility
99 99 a -> b
100 100 a -> c
101 101 a -> d
102 102 $ hg showcopies --config experimental.copies.read-from=filelog-only
103 103
104 104 #endif
105 105
106 106 Check that renames are recorded correctly
107 107
108 108 $ hg mv b b2
109 109 $ hg ci -m 'rename b to b2'
110 110
111 111 #if extra
112 112
113 113 $ hg changesetcopies
114 114 files: b b2
115 115 filesadded: 1
116 116 filesremoved: 0
117 117
118 118 p1copies: 1\x00b (esc)
119 119
120 120 #else
121 121 $ hg debugsidedata -c -v -- -1
122 122 4 sidedata entries
123 123 entry-0010 size 3
124 124 '1\x00b'
125 125 entry-0011 size 0
126 126 ''
127 127 entry-0012 size 1
128 128 '1'
129 129 entry-0013 size 1
130 130 '0'
131 131 #endif
132 132
133 133 $ hg showcopies
134 134 b -> b2
135 135
136 136
137 137 Rename onto existing file. This should get recorded in the changeset files list and in the extras,
138 138 even though there is no filelog entry.
139 139
140 140 $ hg cp b2 c --force
141 141 $ hg st --copies
142 142 M c
143 143 b2
144 144
145 145 #if extra
146 146
147 147 $ hg debugindex c
148 148 rev linkrev nodeid p1 p2
149 149 0 1 b789fdd96dc2 000000000000 000000000000
150 150
151 151 #else
152 152
153 153 $ hg debugindex c
154 154 rev linkrev nodeid p1 p2
155 155 0 1 37d9b5d994ea 000000000000 000000000000
156 156
157 157 #endif
158 158
159 159
160 160 $ hg ci -m 'move b onto d'
161 161
162 162 #if extra
163 163
164 164 $ hg changesetcopies
165 165 files: c
166 166
167 167 p1copies: 0\x00b2 (esc)
168 168
169 169 #else
170 170 $ hg debugsidedata -c -v -- -1
171 171 4 sidedata entries
172 172 entry-0010 size 4
173 173 '0\x00b2'
174 174 entry-0011 size 0
175 175 ''
176 176 entry-0012 size 0
177 177 ''
178 178 entry-0013 size 0
179 179 ''
180 180 #endif
181 181
182 182 $ hg showcopies
183 183 b2 -> c
184 184
185 185 #if extra
186 186
187 187 $ hg debugindex c
188 188 rev linkrev nodeid p1 p2
189 189 0 1 b789fdd96dc2 000000000000 000000000000
190 190
191 191 #else
192 192
193 193 $ hg debugindex c
194 194 rev linkrev nodeid p1 p2
195 195 0 1 37d9b5d994ea 000000000000 000000000000
196 196 1 3 029625640347 000000000000 000000000000
197 197
198 198 #endif
199 199
200 200 Create a merge commit with copying done during merge.
201 201
202 202 $ hg co 0
203 203 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
204 204 $ hg cp a e
205 205 $ hg cp a f
206 206 $ hg ci -m 'copy a to e and f'
207 207 created new head
208 208 $ hg merge 3
209 209 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
210 210 (branch merge, don't forget to commit)
211 211 File 'a' exists on both sides, so 'g' could be recorded as being from p1 or p2, but we currently
212 212 always record it as being from p1
213 213 $ hg cp a g
214 214 File 'd' exists only in p2, so 'h' should be from p2
215 215 $ hg cp d h
216 216 File 'f' exists only in p1, so 'i' should be from p1
217 217 $ hg cp f i
218 218 $ hg ci -m 'merge'
219 219
220 220 #if extra
221 221
222 222 $ hg changesetcopies
223 223 files: g h i
224 224 filesadded: 0
225 225 1
226 226 2
227 227
228 228 p1copies: 0\x00a (esc)
229 229 2\x00f (esc)
230 230 p2copies: 1\x00d (esc)
231 231
232 232 #else
233 233 $ hg debugsidedata -c -v -- -1
234 234 4 sidedata entries
235 235 entry-0010 size 7
236 236 '0\x00a\n2\x00f'
237 237 entry-0011 size 3
238 238 '1\x00d'
239 239 entry-0012 size 5
240 240 '0\n1\n2'
241 241 entry-0013 size 0
242 242 ''
243 243 #endif
244 244
245 245 $ hg showcopies
246 246 a -> g
247 247 d -> h
248 248 f -> i
249 249
250 250 Test writing to both changeset and filelog
251 251
252 252 $ hg cp a j
253 253 #if extra
254 254 $ hg ci -m 'copy a to j' --config experimental.copies.write-to=compatibility
255 255 $ hg changesetcopies
256 256 files: j
257 257 filesadded: 0
258 258 filesremoved:
259 259
260 260 p1copies: 0\x00a (esc)
261 261 p2copies:
262 262 #else
263 263 $ hg ci -m 'copy a to j'
264 264 $ hg debugsidedata -c -v -- -1
265 265 4 sidedata entries
266 266 entry-0010 size 3
267 267 '0\x00a'
268 268 entry-0011 size 0
269 269 ''
270 270 entry-0012 size 1
271 271 '0'
272 272 entry-0013 size 0
273 273 ''
274 274 #endif
275 275 $ hg debugdata j 0
276 276 \x01 (esc)
277 277 copy: a
278 278 copyrev: b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3
279 279 \x01 (esc)
280 280 a
281 281 $ hg showcopies
282 282 a -> j
283 283 $ hg showcopies --config experimental.copies.read-from=compatibility
284 284 a -> j
285 285 $ hg showcopies --config experimental.copies.read-from=filelog-only
286 286 a -> j
287 287 Existing copy information in the changeset gets removed on amend and writing
288 288 copy information on to the filelog
289 289 #if extra
290 290 $ hg ci --amend -m 'copy a to j, v2' \
291 291 > --config experimental.copies.write-to=filelog-only
292 292 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-*-amend.hg (glob)
293 293 $ hg changesetcopies
294 294 files: j
295 295
296 296 #else
297 297 $ hg ci --amend -m 'copy a to j, v2'
298 298 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/*-*-amend.hg (glob)
299 299 $ hg debugsidedata -c -v -- -1
300 300 4 sidedata entries
301 301 entry-0010 size 3
302 302 '0\x00a'
303 303 entry-0011 size 0
304 304 ''
305 305 entry-0012 size 1
306 306 '0'
307 307 entry-0013 size 0
308 308 ''
309 309 #endif
310 310 $ hg showcopies --config experimental.copies.read-from=filelog-only
311 311 a -> j
312 312 The entries should be written to extras even if they're empty (so the client
313 313 won't have to fall back to reading from filelogs)
314 314 $ echo x >> j
315 315 #if extra
316 316 $ hg ci -m 'modify j' --config experimental.copies.write-to=compatibility
317 317 $ hg changesetcopies
318 318 files: j
319 319 filesadded:
320 320 filesremoved:
321 321
322 322 p1copies:
323 323 p2copies:
324 324 #else
325 325 $ hg ci -m 'modify j'
326 326 $ hg debugsidedata -c -v -- -1
327 327 4 sidedata entries
328 328 entry-0010 size 0
329 329 ''
330 330 entry-0011 size 0
331 331 ''
332 332 entry-0012 size 0
333 333 ''
334 334 entry-0013 size 0
335 335 ''
336 336 #endif
337 337
338 338 Test writing only to filelog
339 339
340 340 $ hg cp a k
341 341 #if extra
342 342 $ hg ci -m 'copy a to k' --config experimental.copies.write-to=filelog-only
343 343
344 344 $ hg changesetcopies
345 345 files: k
346 346
347 347 #else
348 348 $ hg ci -m 'copy a to k'
349 349 $ hg debugsidedata -c -v -- -1
350 350 4 sidedata entries
351 351 entry-0010 size 3
352 352 '0\x00a'
353 353 entry-0011 size 0
354 354 ''
355 355 entry-0012 size 1
356 356 '0'
357 357 entry-0013 size 0
358 358 ''
359 359 #endif
360 360
361 361 $ hg debugdata k 0
362 362 \x01 (esc)
363 363 copy: a
364 364 copyrev: b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3
365 365 \x01 (esc)
366 366 a
367 367 #if extra
368 368 $ hg showcopies
369 369
370 370 $ hg showcopies --config experimental.copies.read-from=compatibility
371 371 a -> k
372 372 $ hg showcopies --config experimental.copies.read-from=filelog-only
373 373 a -> k
374 374 #else
375 375 $ hg showcopies
376 376 a -> k
377 377 #endif
378 378
379 379 $ cd ..
380 380
381 381 Test rebasing a commit with copy information
382 382
383 383 $ hg init rebase-rename
384 384 $ cd rebase-rename
385 385 $ echo a > a
386 386 $ hg ci -Aqm 'add a'
387 387 $ echo a2 > a
388 388 $ hg ci -m 'modify a'
389 389 $ hg co -q 0
390 390 $ hg mv a b
391 391 $ hg ci -qm 'rename a to b'
392 392 $ hg rebase -d 1 --config rebase.experimental.inmemory=yes
393 393 rebasing 2:* "rename a to b" (tip) (glob)
394 394 merging a and b to b
395 395 saved backup bundle to $TESTTMP/rebase-rename/.hg/strip-backup/*-*-rebase.hg (glob)
396 396 $ hg st --change . --copies
397 397 A b
398 398 a
399 399 R a
400 400 $ cd ..
401 401
402 402 Test splitting a commit
403 403
404 404 $ hg init split
405 405 $ cd split
406 406 $ echo a > a
407 407 $ echo b > b
408 408 $ hg ci -Aqm 'add a and b'
409 409 $ echo a2 > a
410 410 $ hg mv b c
411 411 $ hg ci -m 'modify a, move b to c'
412 412 $ hg --config ui.interactive=yes split <<EOF
413 413 > y
414 414 > y
415 415 > n
416 416 > y
417 417 > EOF
418 418 diff --git a/a b/a
419 419 1 hunks, 1 lines changed
420 420 examine changes to 'a'?
421 421 (enter ? for help) [Ynesfdaq?] y
422 422
423 423 @@ -1,1 +1,1 @@
424 424 -a
425 425 +a2
426 426 record this change to 'a'?
427 427 (enter ? for help) [Ynesfdaq?] y
428 428
429 429 diff --git a/b b/c
430 430 rename from b
431 431 rename to c
432 432 examine changes to 'b' and 'c'?
433 433 (enter ? for help) [Ynesfdaq?] n
434 434
435 435 created new head
436 436 diff --git a/b b/c
437 437 rename from b
438 438 rename to c
439 439 examine changes to 'b' and 'c'?
440 440 (enter ? for help) [Ynesfdaq?] y
441 441
442 442 saved backup bundle to $TESTTMP/split/.hg/strip-backup/*-*-split.hg (glob)
443 443 $ cd ..
444 444
445 445 Test committing half a rename
446 446
447 447 $ hg init partial
448 448 $ cd partial
449 449 $ echo a > a
450 450 $ hg ci -Aqm 'add a'
451 451 $ hg mv a b
452 452 $ hg ci -m 'remove a' a
453
454 #if sidedata
455
456 Test upgrading/downgrading to sidedata storage
457 ==============================================
458
459 downgrading (keeping some sidedata)
460
461 $ hg debugformat -v
462 format-variant repo config default
463 fncache: yes yes yes
464 dotencode: yes yes yes
465 generaldelta: yes yes yes
466 sparserevlog: yes yes yes
467 sidedata: yes yes no
468 copies-sdc: yes yes no
469 plain-cl-delta: yes yes yes
470 compression: zlib zlib zlib
471 compression-level: default default default
472 $ hg debugsidedata -c -- 0
473 4 sidedata entries
474 entry-0010 size 0
475 entry-0011 size 0
476 entry-0012 size 1
477 entry-0013 size 0
478 $ hg debugsidedata -c -- 1
479 4 sidedata entries
480 entry-0010 size 0
481 entry-0011 size 0
482 entry-0012 size 0
483 entry-0013 size 1
484 $ hg debugsidedata -m -- 0
485 $ cat << EOF > .hg/hgrc
486 > [format]
487 > use-side-data = yes
488 > exp-use-copies-side-data-changeset = no
489 > EOF
490 $ hg debugupgraderepo --run --quiet --no-backup > /dev/null
491 $ hg debugformat -v
492 format-variant repo config default
493 fncache: yes yes yes
494 dotencode: yes yes yes
495 generaldelta: yes yes yes
496 sparserevlog: yes yes yes
497 sidedata: yes yes no
498 copies-sdc: no no no
499 plain-cl-delta: yes yes yes
500 compression: zlib zlib zlib
501 compression-level: default default default
502 $ hg debugsidedata -c -- 0
503 $ hg debugsidedata -c -- 1
504 $ hg debugsidedata -m -- 0
505
506 upgrading
507
508 $ cat << EOF > .hg/hgrc
509 > [format]
510 > exp-use-copies-side-data-changeset = yes
511 > EOF
512 $ hg debugupgraderepo --run --quiet --no-backup > /dev/null
513 $ hg debugformat -v
514 format-variant repo config default
515 fncache: yes yes yes
516 dotencode: yes yes yes
517 generaldelta: yes yes yes
518 sparserevlog: yes yes yes
519 sidedata: yes yes no
520 copies-sdc: yes yes no
521 plain-cl-delta: yes yes yes
522 compression: zlib zlib zlib
523 compression-level: default default default
524 $ hg debugsidedata -c -- 0
525 4 sidedata entries
526 entry-0010 size 0
527 entry-0011 size 0
528 entry-0012 size 1
529 entry-0013 size 0
530 $ hg debugsidedata -c -- 1
531 4 sidedata entries
532 entry-0010 size 0
533 entry-0011 size 0
534 entry-0012 size 0
535 entry-0013 size 1
536 $ hg debugsidedata -m -- 0
537
538 #endif
539
453 540 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now