168 |
168 |
169 def _dirstatecopies(repo, match=None): |
169 def _dirstatecopies(repo, match=None): |
170 ds = repo.dirstate |
170 ds = repo.dirstate |
171 c = ds.copies().copy() |
171 c = ds.copies().copy() |
172 for k in list(c): |
172 for k in list(c): |
173 if ds[k] not in 'anm' or (match and not match(k)): |
173 if ds[k] not in b'anm' or (match and not match(k)): |
174 del c[k] |
174 del c[k] |
175 return c |
175 return c |
176 |
176 |
177 |
177 |
178 def _computeforwardmissing(a, b, match=None): |
178 def _computeforwardmissing(a, b, match=None): |
185 return mb.filesnotin(ma, match=match) |
185 return mb.filesnotin(ma, match=match) |
186 |
186 |
187 |
187 |
188 def usechangesetcentricalgo(repo): |
188 def usechangesetcentricalgo(repo): |
189 """Checks if we should use changeset-centric copy algorithms""" |
189 """Checks if we should use changeset-centric copy algorithms""" |
190 readfrom = repo.ui.config('experimental', 'copies.read-from') |
190 readfrom = repo.ui.config(b'experimental', b'copies.read-from') |
191 changesetsource = ('changeset-only', 'compatibility') |
191 changesetsource = (b'changeset-only', b'compatibility') |
192 return readfrom in changesetsource |
192 return readfrom in changesetsource |
193 |
193 |
194 |
194 |
195 def _committedforwardcopies(a, b, base, match): |
195 def _committedforwardcopies(a, b, base, match): |
196 """Like _forwardcopies(), but b.rev() cannot be None (working copy)""" |
196 """Like _forwardcopies(), but b.rev() cannot be None (working copy)""" |
199 repo = a._repo |
199 repo = a._repo |
200 |
200 |
201 if usechangesetcentricalgo(repo): |
201 if usechangesetcentricalgo(repo): |
202 return _changesetforwardcopies(a, b, match) |
202 return _changesetforwardcopies(a, b, match) |
203 |
203 |
204 debug = repo.ui.debugflag and repo.ui.configbool('devel', 'debug.copies') |
204 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies') |
205 dbg = repo.ui.debug |
205 dbg = repo.ui.debug |
206 if debug: |
206 if debug: |
207 dbg('debug.copies: looking into rename from %s to %s\n' % (a, b)) |
207 dbg(b'debug.copies: looking into rename from %s to %s\n' % (a, b)) |
208 limit = _findlimit(repo, a, b) |
208 limit = _findlimit(repo, a, b) |
209 if debug: |
209 if debug: |
210 dbg('debug.copies: search limit: %d\n' % limit) |
210 dbg(b'debug.copies: search limit: %d\n' % limit) |
211 am = a.manifest() |
211 am = a.manifest() |
212 basemf = None if base is None else base.manifest() |
212 basemf = None if base is None else base.manifest() |
213 |
213 |
214 # find where new files came from |
214 # find where new files came from |
215 # we currently don't try to find where old files went, too expensive |
215 # we currently don't try to find where old files went, too expensive |
229 missing = _computeforwardmissing(a, b, match=forwardmissingmatch) |
229 missing = _computeforwardmissing(a, b, match=forwardmissingmatch) |
230 |
230 |
231 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True) |
231 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True) |
232 |
232 |
233 if debug: |
233 if debug: |
234 dbg('debug.copies: missing files to search: %d\n' % len(missing)) |
234 dbg(b'debug.copies: missing files to search: %d\n' % len(missing)) |
235 |
235 |
236 for f in sorted(missing): |
236 for f in sorted(missing): |
237 if debug: |
237 if debug: |
238 dbg('debug.copies: tracing file: %s\n' % f) |
238 dbg(b'debug.copies: tracing file: %s\n' % f) |
239 fctx = b[f] |
239 fctx = b[f] |
240 fctx._ancestrycontext = ancestrycontext |
240 fctx._ancestrycontext = ancestrycontext |
241 |
241 |
242 if debug: |
242 if debug: |
243 start = util.timer() |
243 start = util.timer() |
244 opath = _tracefile(fctx, am, basemf, limit) |
244 opath = _tracefile(fctx, am, basemf, limit) |
245 if opath: |
245 if opath: |
246 if debug: |
246 if debug: |
247 dbg('debug.copies: rename of: %s\n' % opath) |
247 dbg(b'debug.copies: rename of: %s\n' % opath) |
248 cm[f] = opath |
248 cm[f] = opath |
249 if debug: |
249 if debug: |
250 dbg( |
250 dbg( |
251 'debug.copies: time: %f seconds\n' |
251 b'debug.copies: time: %f seconds\n' |
252 % (util.timer() - start) |
252 % (util.timer() - start) |
253 ) |
253 ) |
254 return cm |
254 return cm |
255 |
255 |
256 |
256 |
340 copies = _committedforwardcopies(a, b, base, match) |
340 copies = _committedforwardcopies(a, b, base, match) |
341 return copies |
341 return copies |
342 |
342 |
343 |
343 |
344 def _backwardrenames(a, b, match): |
344 def _backwardrenames(a, b, match): |
345 if a._repo.ui.config('experimental', 'copytrace') == 'off': |
345 if a._repo.ui.config(b'experimental', b'copytrace') == b'off': |
346 return {} |
346 return {} |
347 |
347 |
348 # Even though we're not taking copies into account, 1:n rename situations |
348 # Even though we're not taking copies into account, 1:n rename situations |
349 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we |
349 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we |
350 # arbitrarily pick one of the renames. |
350 # arbitrarily pick one of the renames. |
364 |
364 |
365 |
365 |
366 def pathcopies(x, y, match=None): |
366 def pathcopies(x, y, match=None): |
367 """find {dst@y: src@x} copy mapping for directed compare""" |
367 """find {dst@y: src@x} copy mapping for directed compare""" |
368 repo = x._repo |
368 repo = x._repo |
369 debug = repo.ui.debugflag and repo.ui.configbool('devel', 'debug.copies') |
369 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies') |
370 if debug: |
370 if debug: |
371 repo.ui.debug('debug.copies: searching copies from %s to %s\n' % (x, y)) |
371 repo.ui.debug( |
|
372 b'debug.copies: searching copies from %s to %s\n' % (x, y) |
|
373 ) |
372 if x == y or not x or not y: |
374 if x == y or not x or not y: |
373 return {} |
375 return {} |
374 a = y.ancestor(x) |
376 a = y.ancestor(x) |
375 if a == x: |
377 if a == x: |
376 if debug: |
378 if debug: |
377 repo.ui.debug('debug.copies: search mode: forward\n') |
379 repo.ui.debug(b'debug.copies: search mode: forward\n') |
378 if y.rev() is None and x == y.p1(): |
380 if y.rev() is None and x == y.p1(): |
379 # short-circuit to avoid issues with merge states |
381 # short-circuit to avoid issues with merge states |
380 return _dirstatecopies(repo, match) |
382 return _dirstatecopies(repo, match) |
381 copies = _forwardcopies(x, y, match=match) |
383 copies = _forwardcopies(x, y, match=match) |
382 elif a == y: |
384 elif a == y: |
383 if debug: |
385 if debug: |
384 repo.ui.debug('debug.copies: search mode: backward\n') |
386 repo.ui.debug(b'debug.copies: search mode: backward\n') |
385 copies = _backwardrenames(x, y, match=match) |
387 copies = _backwardrenames(x, y, match=match) |
386 else: |
388 else: |
387 if debug: |
389 if debug: |
388 repo.ui.debug('debug.copies: search mode: combined\n') |
390 repo.ui.debug(b'debug.copies: search mode: combined\n') |
389 base = None |
391 base = None |
390 if a.rev() != node.nullrev: |
392 if a.rev() != node.nullrev: |
391 base = x |
393 base = x |
392 copies = _chain( |
394 copies = _chain( |
393 _backwardrenames(x, a, match=match), |
395 _backwardrenames(x, a, match=match), |
451 |
453 |
452 # avoid silly behavior for parent -> working dir |
454 # avoid silly behavior for parent -> working dir |
453 if c2.node() is None and c1.node() == repo.dirstate.p1(): |
455 if c2.node() is None and c1.node() == repo.dirstate.p1(): |
454 return _dirstatecopies(repo, narrowmatch), {}, {}, {}, {} |
456 return _dirstatecopies(repo, narrowmatch), {}, {}, {}, {} |
455 |
457 |
456 copytracing = repo.ui.config('experimental', 'copytrace') |
458 copytracing = repo.ui.config(b'experimental', b'copytrace') |
457 if stringutil.parsebool(copytracing) is False: |
459 if stringutil.parsebool(copytracing) is False: |
458 # stringutil.parsebool() returns None when it is unable to parse the |
460 # stringutil.parsebool() returns None when it is unable to parse the |
459 # value, so we should rely on making sure copytracing is on such cases |
461 # value, so we should rely on making sure copytracing is on such cases |
460 return {}, {}, {}, {}, {} |
462 return {}, {}, {}, {}, {} |
461 |
463 |
464 return _fullcopytracing(repo, c1, c2, base) |
466 return _fullcopytracing(repo, c1, c2, base) |
465 |
467 |
466 # Copy trace disabling is explicitly below the node == p1 logic above |
468 # Copy trace disabling is explicitly below the node == p1 logic above |
467 # because the logic above is required for a simple copy to be kept across a |
469 # because the logic above is required for a simple copy to be kept across a |
468 # rebase. |
470 # rebase. |
469 if copytracing == 'heuristics': |
471 if copytracing == b'heuristics': |
470 # Do full copytracing if only non-public revisions are involved as |
472 # Do full copytracing if only non-public revisions are involved as |
471 # that will be fast enough and will also cover the copies which could |
473 # that will be fast enough and will also cover the copies which could |
472 # be missed by heuristics |
474 # be missed by heuristics |
473 if _isfullcopytraceable(repo, c1, base): |
475 if _isfullcopytraceable(repo, c1, base): |
474 return _fullcopytracing(repo, c1, c2, base) |
476 return _fullcopytracing(repo, c1, c2, base) |
488 """ |
490 """ |
489 if c1.rev() is None: |
491 if c1.rev() is None: |
490 c1 = c1.p1() |
492 c1 = c1.p1() |
491 if c1.mutable() and base.mutable(): |
493 if c1.mutable() and base.mutable(): |
492 sourcecommitlimit = repo.ui.configint( |
494 sourcecommitlimit = repo.ui.configint( |
493 'experimental', 'copytrace.sourcecommitlimit' |
495 b'experimental', b'copytrace.sourcecommitlimit' |
494 ) |
496 ) |
495 commits = len(repo.revs('%d::%d', base.rev(), c1.rev())) |
497 commits = len(repo.revs(b'%d::%d', base.rev(), c1.rev())) |
496 return commits < sourcecommitlimit |
498 return commits < sourcecommitlimit |
497 return False |
499 return False |
498 |
500 |
499 |
501 |
500 def _checksinglesidecopies( |
502 def _checksinglesidecopies( |
590 addedinm1 = m1.filesnotin(mb, repo.narrowmatch()) |
592 addedinm1 = m1.filesnotin(mb, repo.narrowmatch()) |
591 addedinm2 = m2.filesnotin(mb, repo.narrowmatch()) |
593 addedinm2 = m2.filesnotin(mb, repo.narrowmatch()) |
592 u1 = sorted(addedinm1 - addedinm2) |
594 u1 = sorted(addedinm1 - addedinm2) |
593 u2 = sorted(addedinm2 - addedinm1) |
595 u2 = sorted(addedinm2 - addedinm1) |
594 |
596 |
595 header = " unmatched files in %s" |
597 header = b" unmatched files in %s" |
596 if u1: |
598 if u1: |
597 repo.ui.debug("%s:\n %s\n" % (header % 'local', "\n ".join(u1))) |
599 repo.ui.debug(b"%s:\n %s\n" % (header % b'local', b"\n ".join(u1))) |
598 if u2: |
600 if u2: |
599 repo.ui.debug("%s:\n %s\n" % (header % 'other', "\n ".join(u2))) |
601 repo.ui.debug(b"%s:\n %s\n" % (header % b'other', b"\n ".join(u2))) |
600 |
602 |
601 fullcopy = copies1.copy() |
603 fullcopy = copies1.copy() |
602 fullcopy.update(copies2) |
604 fullcopy.update(copies2) |
603 if not fullcopy: |
605 if not fullcopy: |
604 return copy, {}, diverge, renamedelete, {} |
606 return copy, {}, diverge, renamedelete, {} |
605 |
607 |
606 if repo.ui.debugflag: |
608 if repo.ui.debugflag: |
607 repo.ui.debug( |
609 repo.ui.debug( |
608 " all copies found (* = to merge, ! = divergent, " |
610 b" all copies found (* = to merge, ! = divergent, " |
609 "% = renamed and deleted):\n" |
611 b"% = renamed and deleted):\n" |
610 ) |
612 ) |
611 for f in sorted(fullcopy): |
613 for f in sorted(fullcopy): |
612 note = "" |
614 note = b"" |
613 if f in copy: |
615 if f in copy: |
614 note += "*" |
616 note += b"*" |
615 if f in divergeset: |
617 if f in divergeset: |
616 note += "!" |
618 note += b"!" |
617 if f in renamedeleteset: |
619 if f in renamedeleteset: |
618 note += "%" |
620 note += b"%" |
619 repo.ui.debug( |
621 repo.ui.debug( |
620 " src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f, note) |
622 b" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f, note) |
621 ) |
623 ) |
622 del divergeset |
624 del divergeset |
623 |
625 |
624 repo.ui.debug(" checking for directory renames\n") |
626 repo.ui.debug(b" checking for directory renames\n") |
625 |
627 |
626 # generate a directory move map |
628 # generate a directory move map |
627 d1, d2 = c1.dirs(), c2.dirs() |
629 d1, d2 = c1.dirs(), c2.dirs() |
628 invalid = set() |
630 invalid = set() |
629 dirmove = {} |
631 dirmove = {} |
654 del d1, d2, invalid |
656 del d1, d2, invalid |
655 |
657 |
656 if not dirmove: |
658 if not dirmove: |
657 return copy, {}, diverge, renamedelete, {} |
659 return copy, {}, diverge, renamedelete, {} |
658 |
660 |
659 dirmove = {k + "/": v + "/" for k, v in dirmove.iteritems()} |
661 dirmove = {k + b"/": v + b"/" for k, v in dirmove.iteritems()} |
660 |
662 |
661 for d in dirmove: |
663 for d in dirmove: |
662 repo.ui.debug( |
664 repo.ui.debug( |
663 " discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d]) |
665 b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d]) |
664 ) |
666 ) |
665 |
667 |
666 movewithdir = {} |
668 movewithdir = {} |
667 # check unaccounted nonoverlapping files against directory moves |
669 # check unaccounted nonoverlapping files against directory moves |
668 for f in u1 + u2: |
670 for f in u1 + u2: |
672 # new file added in a directory that was moved, move it |
674 # new file added in a directory that was moved, move it |
673 df = dirmove[d] + f[len(d) :] |
675 df = dirmove[d] + f[len(d) :] |
674 if df not in copy: |
676 if df not in copy: |
675 movewithdir[f] = df |
677 movewithdir[f] = df |
676 repo.ui.debug( |
678 repo.ui.debug( |
677 (" pending file src: '%s' -> " "dst: '%s'\n") |
679 (b" pending file src: '%s' -> " b"dst: '%s'\n") |
678 % (f, df) |
680 % (f, df) |
679 ) |
681 ) |
680 break |
682 break |
681 |
683 |
682 return copy, movewithdir, diverge, renamedelete, dirmove |
684 return copy, movewithdir, diverge, renamedelete, dirmove |
714 |
716 |
715 copies = {} |
717 copies = {} |
716 |
718 |
717 changedfiles = set() |
719 changedfiles = set() |
718 m1 = c1.manifest() |
720 m1 = c1.manifest() |
719 if not repo.revs('%d::%d', base.rev(), c2.rev()): |
721 if not repo.revs(b'%d::%d', base.rev(), c2.rev()): |
720 # If base is not in c2 branch, we switch to fullcopytracing |
722 # If base is not in c2 branch, we switch to fullcopytracing |
721 repo.ui.debug( |
723 repo.ui.debug( |
722 "switching to full copytracing as base is not " |
724 b"switching to full copytracing as base is not " |
723 "an ancestor of c2\n" |
725 b"an ancestor of c2\n" |
724 ) |
726 ) |
725 return _fullcopytracing(repo, c1, c2, base) |
727 return _fullcopytracing(repo, c1, c2, base) |
726 |
728 |
727 ctx = c2 |
729 ctx = c2 |
728 while ctx != base: |
730 while ctx != base: |
729 if len(ctx.parents()) == 2: |
731 if len(ctx.parents()) == 2: |
730 # To keep things simple let's not handle merges |
732 # To keep things simple let's not handle merges |
731 repo.ui.debug("switching to full copytracing because of merges\n") |
733 repo.ui.debug(b"switching to full copytracing because of merges\n") |
732 return _fullcopytracing(repo, c1, c2, base) |
734 return _fullcopytracing(repo, c1, c2, base) |
733 changedfiles.update(ctx.files()) |
735 changedfiles.update(ctx.files()) |
734 ctx = ctx.p1() |
736 ctx = ctx.p1() |
735 |
737 |
736 cp = _forwardcopies(base, c2) |
738 cp = _forwardcopies(base, c2) |
765 # c2.filectx(f) won't fail |
767 # c2.filectx(f) won't fail |
766 f2 = c2.filectx(f) |
768 f2 = c2.filectx(f) |
767 # we can have a lot of candidates which can slow down the heuristics |
769 # we can have a lot of candidates which can slow down the heuristics |
768 # config value to limit the number of candidates moves to check |
770 # config value to limit the number of candidates moves to check |
769 maxcandidates = repo.ui.configint( |
771 maxcandidates = repo.ui.configint( |
770 'experimental', 'copytrace.movecandidateslimit' |
772 b'experimental', b'copytrace.movecandidateslimit' |
771 ) |
773 ) |
772 |
774 |
773 if len(movecandidates) > maxcandidates: |
775 if len(movecandidates) > maxcandidates: |
774 repo.ui.status( |
776 repo.ui.status( |
775 _( |
777 _( |
776 "skipping copytracing for '%s', more " |
778 b"skipping copytracing for '%s', more " |
777 "candidates than the limit: %d\n" |
779 b"candidates than the limit: %d\n" |
778 ) |
780 ) |
779 % (f, len(movecandidates)) |
781 % (f, len(movecandidates)) |
780 ) |
782 ) |
781 continue |
783 continue |
782 |
784 |
831 filter copy records. Any copies that occur between fromrev and |
833 filter copy records. Any copies that occur between fromrev and |
832 skiprev will not be duplicated, even if they appear in the set of |
834 skiprev will not be duplicated, even if they appear in the set of |
833 copies between fromrev and rev. |
835 copies between fromrev and rev. |
834 """ |
836 """ |
835 exclude = {} |
837 exclude = {} |
836 ctraceconfig = repo.ui.config('experimental', 'copytrace') |
838 ctraceconfig = repo.ui.config(b'experimental', b'copytrace') |
837 bctrace = stringutil.parsebool(ctraceconfig) |
839 bctrace = stringutil.parsebool(ctraceconfig) |
838 if skiprev is not None and ( |
840 if skiprev is not None and ( |
839 ctraceconfig == 'heuristics' or bctrace or bctrace is None |
841 ctraceconfig == b'heuristics' or bctrace or bctrace is None |
840 ): |
842 ): |
841 # copytrace='off' skips this line, but not the entire function because |
843 # copytrace='off' skips this line, but not the entire function because |
842 # the line below is O(size of the repo) during a rebase, while the rest |
844 # the line below is O(size of the repo) during a rebase, while the rest |
843 # of the function is much faster (and is required for carrying copy |
845 # of the function is much faster (and is required for carrying copy |
844 # metadata across the rebase anyway). |
846 # metadata across the rebase anyway). |