##// END OF EJS Templates
copies: filter invalid copies only at end of pathcopies() (issue6163)...
Martin von Zweigbergk -
r42797:d013099c default
parent child Browse files
Show More
@@ -1,805 +1,803 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 16 from . import (
17 17 match as matchmod,
18 18 node,
19 19 pathutil,
20 20 util,
21 21 )
22 22 from .utils import (
23 23 stringutil,
24 24 )
25 25
26 26 def _findlimit(repo, ctxa, ctxb):
27 27 """
28 28 Find the last revision that needs to be checked to ensure that a full
29 29 transitive closure for file copies can be properly calculated.
30 30 Generally, this means finding the earliest revision number that's an
31 31 ancestor of a or b but not both, except when a or b is a direct descendent
32 32 of the other, in which case we can return the minimum revnum of a and b.
33 33 """
34 34
35 35 # basic idea:
36 36 # - mark a and b with different sides
37 37 # - if a parent's children are all on the same side, the parent is
38 38 # on that side, otherwise it is on no side
39 39 # - walk the graph in topological order with the help of a heap;
40 40 # - add unseen parents to side map
41 41 # - clear side of any parent that has children on different sides
42 42 # - track number of interesting revs that might still be on a side
43 43 # - track the lowest interesting rev seen
44 44 # - quit when interesting revs is zero
45 45
46 46 cl = repo.changelog
47 47 wdirparents = None
48 48 a = ctxa.rev()
49 49 b = ctxb.rev()
50 50 if a is None:
51 51 wdirparents = (ctxa.p1(), ctxa.p2())
52 52 a = node.wdirrev
53 53 if b is None:
54 54 assert not wdirparents
55 55 wdirparents = (ctxb.p1(), ctxb.p2())
56 56 b = node.wdirrev
57 57
58 58 side = {a: -1, b: 1}
59 59 visit = [-a, -b]
60 60 heapq.heapify(visit)
61 61 interesting = len(visit)
62 62 limit = node.wdirrev
63 63
64 64 while interesting:
65 65 r = -heapq.heappop(visit)
66 66 if r == node.wdirrev:
67 67 parents = [pctx.rev() for pctx in wdirparents]
68 68 else:
69 69 parents = cl.parentrevs(r)
70 70 if parents[1] == node.nullrev:
71 71 parents = parents[:1]
72 72 for p in parents:
73 73 if p not in side:
74 74 # first time we see p; add it to visit
75 75 side[p] = side[r]
76 76 if side[p]:
77 77 interesting += 1
78 78 heapq.heappush(visit, -p)
79 79 elif side[p] and side[p] != side[r]:
80 80 # p was interesting but now we know better
81 81 side[p] = 0
82 82 interesting -= 1
83 83 if side[r]:
84 84 limit = r # lowest rev visited
85 85 interesting -= 1
86 86
87 87 # Consider the following flow (see test-commit-amend.t under issue4405):
88 88 # 1/ File 'a0' committed
89 89 # 2/ File renamed from 'a0' to 'a1' in a new commit (call it 'a1')
90 90 # 3/ Move back to first commit
91 91 # 4/ Create a new commit via revert to contents of 'a1' (call it 'a1-amend')
92 92 # 5/ Rename file from 'a1' to 'a2' and commit --amend 'a1-msg'
93 93 #
94 94 # During the amend in step five, we will be in this state:
95 95 #
96 96 # @ 3 temporary amend commit for a1-amend
97 97 # |
98 98 # o 2 a1-amend
99 99 # |
100 100 # | o 1 a1
101 101 # |/
102 102 # o 0 a0
103 103 #
104 104 # When _findlimit is called, a and b are revs 3 and 0, so limit will be 2,
105 105 # yet the filelog has the copy information in rev 1 and we will not look
106 106 # back far enough unless we also look at the a and b as candidates.
107 107 # This only occurs when a is a descendent of b or visa-versa.
108 108 return min(limit, a, b)
109 109
110 110 def _filter(src, dst, t):
111 111 """filters out invalid copies after chaining"""
112 112
113 113 # When _chain()'ing copies in 'a' (from 'src' via some other commit 'mid')
114 114 # with copies in 'b' (from 'mid' to 'dst'), we can get the different cases
115 115 # in the following table (not including trivial cases). For example, case 2
116 116 # is where a file existed in 'src' and remained under that name in 'mid' and
117 117 # then was renamed between 'mid' and 'dst'.
118 118 #
119 119 # case src mid dst result
120 120 # 1 x y - -
121 121 # 2 x y y x->y
122 122 # 3 x y x -
123 123 # 4 x y z x->z
124 124 # 5 - x y -
125 125 # 6 x x y x->y
126 126 #
127 127 # _chain() takes care of chaining the copies in 'a' and 'b', but it
128 128 # cannot tell the difference between cases 1 and 2, between 3 and 4, or
129 129 # between 5 and 6, so it includes all cases in its result.
130 130 # Cases 1, 3, and 5 are then removed by _filter().
131 131
132 132 for k, v in list(t.items()):
133 133 # remove copies from files that didn't exist
134 134 if v not in src:
135 135 del t[k]
136 136 # remove criss-crossed copies
137 137 elif k in src and v in dst:
138 138 del t[k]
139 139 # remove copies to files that were then removed
140 140 elif k not in dst:
141 141 del t[k]
142 142
143 143 def _chain(a, b):
144 144 """chain two sets of copies 'a' and 'b'"""
145 145 t = a.copy()
146 146 for k, v in b.iteritems():
147 147 if v in t:
148 148 t[k] = t[v]
149 149 else:
150 150 t[k] = v
151 151 return t
152 152
153 153 def _tracefile(fctx, am, limit):
154 154 """return file context that is the ancestor of fctx present in ancestor
155 155 manifest am, stopping after the first ancestor lower than limit"""
156 156
157 157 for f in fctx.ancestors():
158 158 path = f.path()
159 159 if am.get(path, None) == f.filenode():
160 160 return path
161 161 if not f.isintroducedafter(limit):
162 162 return None
163 163
164 164 def _dirstatecopies(repo, match=None):
165 165 ds = repo.dirstate
166 166 c = ds.copies().copy()
167 167 for k in list(c):
168 168 if ds[k] not in 'anm' or (match and not match(k)):
169 169 del c[k]
170 170 return c
171 171
172 172 def _computeforwardmissing(a, b, match=None):
173 173 """Computes which files are in b but not a.
174 174 This is its own function so extensions can easily wrap this call to see what
175 175 files _forwardcopies is about to process.
176 176 """
177 177 ma = a.manifest()
178 178 mb = b.manifest()
179 179 return mb.filesnotin(ma, match=match)
180 180
181 181 def usechangesetcentricalgo(repo):
182 182 """Checks if we should use changeset-centric copy algorithms"""
183 183 return (repo.ui.config('experimental', 'copies.read-from') in
184 184 ('changeset-only', 'compatibility'))
185 185
186 186 def _committedforwardcopies(a, b, match):
187 187 """Like _forwardcopies(), but b.rev() cannot be None (working copy)"""
188 188 # files might have to be traced back to the fctx parent of the last
189 189 # one-side-only changeset, but not further back than that
190 190 repo = a._repo
191 191
192 192 if usechangesetcentricalgo(repo):
193 193 return _changesetforwardcopies(a, b, match)
194 194
195 195 debug = repo.ui.debugflag and repo.ui.configbool('devel', 'debug.copies')
196 196 dbg = repo.ui.debug
197 197 if debug:
198 198 dbg('debug.copies: looking into rename from %s to %s\n'
199 199 % (a, b))
200 200 limit = _findlimit(repo, a, b)
201 201 if debug:
202 202 dbg('debug.copies: search limit: %d\n' % limit)
203 203 am = a.manifest()
204 204
205 205 # find where new files came from
206 206 # we currently don't try to find where old files went, too expensive
207 207 # this means we can miss a case like 'hg rm b; hg cp a b'
208 208 cm = {}
209 209
210 210 # Computing the forward missing is quite expensive on large manifests, since
211 211 # it compares the entire manifests. We can optimize it in the common use
212 212 # case of computing what copies are in a commit versus its parent (like
213 213 # during a rebase or histedit). Note, we exclude merge commits from this
214 214 # optimization, since the ctx.files() for a merge commit is not correct for
215 215 # this comparison.
216 216 forwardmissingmatch = match
217 217 if b.p1() == a and b.p2().node() == node.nullid:
218 218 filesmatcher = matchmod.exact(b.files())
219 219 forwardmissingmatch = matchmod.intersectmatchers(match, filesmatcher)
220 220 missing = _computeforwardmissing(a, b, match=forwardmissingmatch)
221 221
222 222 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
223 223
224 224 if debug:
225 225 dbg('debug.copies: missing files to search: %d\n' % len(missing))
226 226
227 227 for f in sorted(missing):
228 228 if debug:
229 229 dbg('debug.copies: tracing file: %s\n' % f)
230 230 fctx = b[f]
231 231 fctx._ancestrycontext = ancestrycontext
232 232
233 233 if debug:
234 234 start = util.timer()
235 235 opath = _tracefile(fctx, am, limit)
236 236 if opath:
237 237 if debug:
238 238 dbg('debug.copies: rename of: %s\n' % opath)
239 239 cm[f] = opath
240 240 if debug:
241 241 dbg('debug.copies: time: %f seconds\n'
242 242 % (util.timer() - start))
243 243 return cm
244 244
245 245 def _changesetforwardcopies(a, b, match):
246 246 if a.rev() == node.nullrev:
247 247 return {}
248 248
249 249 repo = a.repo()
250 250 children = {}
251 251 cl = repo.changelog
252 252 missingrevs = cl.findmissingrevs(common=[a.rev()], heads=[b.rev()])
253 253 for r in missingrevs:
254 254 for p in cl.parentrevs(r):
255 255 if p == node.nullrev:
256 256 continue
257 257 if p not in children:
258 258 children[p] = [r]
259 259 else:
260 260 children[p].append(r)
261 261
262 262 roots = set(children) - set(missingrevs)
263 263 # 'work' contains 3-tuples of a (revision number, parent number, copies).
264 264 # The parent number is only used for knowing which parent the copies dict
265 265 # came from.
266 266 # NOTE: To reduce costly copying the 'copies' dicts, we reuse the same
267 267 # instance for *one* of the child nodes (the last one). Once an instance
268 268 # has been put on the queue, it is thus no longer safe to modify it.
269 269 # Conversely, it *is* safe to modify an instance popped off the queue.
270 270 work = [(r, 1, {}) for r in roots]
271 271 heapq.heapify(work)
272 272 alwaysmatch = match.always()
273 273 while work:
274 274 r, i1, copies = heapq.heappop(work)
275 275 if work and work[0][0] == r:
276 276 # We are tracing copies from both parents
277 277 r, i2, copies2 = heapq.heappop(work)
278 278 for dst, src in copies2.items():
279 279 # Unlike when copies are stored in the filelog, we consider
280 280 # it a copy even if the destination already existed on the
281 281 # other branch. It's simply too expensive to check if the
282 282 # file existed in the manifest.
283 283 if dst not in copies:
284 284 # If it was copied on the p1 side, leave it as copied from
285 285 # that side, even if it was also copied on the p2 side.
286 286 copies[dst] = copies2[dst]
287 287 if r == b.rev():
288 _filter(a, b, copies)
289 288 return copies
290 289 for i, c in enumerate(children[r]):
291 290 childctx = repo[c]
292 291 if r == childctx.p1().rev():
293 292 parent = 1
294 293 childcopies = childctx.p1copies()
295 294 else:
296 295 assert r == childctx.p2().rev()
297 296 parent = 2
298 297 childcopies = childctx.p2copies()
299 298 if not alwaysmatch:
300 299 childcopies = {dst: src for dst, src in childcopies.items()
301 300 if match(dst)}
302 301 # Copy the dict only if later iterations will also need it
303 302 if i != len(children[r]) - 1:
304 303 newcopies = copies.copy()
305 304 else:
306 305 newcopies = copies
307 306 if childcopies:
308 307 newcopies = _chain(newcopies, childcopies)
309 308 for f in childctx.filesremoved():
310 309 if f in newcopies:
311 310 del newcopies[f]
312 311 heapq.heappush(work, (c, parent, newcopies))
313 312 assert False
314 313
315 314 def _forwardcopies(a, b, match=None):
316 315 """find {dst@b: src@a} copy mapping where a is an ancestor of b"""
317 316
318 317 match = a.repo().narrowmatch(match)
319 318 # check for working copy
320 319 if b.rev() is None:
321 320 cm = _committedforwardcopies(a, b.p1(), match)
322 321 # combine copies from dirstate if necessary
323 322 copies = _chain(cm, _dirstatecopies(b._repo, match))
324 _filter(a, b, copies)
325 323 else:
326 324 copies = _committedforwardcopies(a, b, match)
327 325 return copies
328 326
329 327 def _backwardrenames(a, b, match):
330 328 if a._repo.ui.config('experimental', 'copytrace') == 'off':
331 329 return {}
332 330
333 331 # Even though we're not taking copies into account, 1:n rename situations
334 332 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
335 333 # arbitrarily pick one of the renames.
336 334 # We don't want to pass in "match" here, since that would filter
337 335 # the destination by it. Since we're reversing the copies, we want
338 336 # to filter the source instead.
339 337 f = _forwardcopies(b, a)
340 338 r = {}
341 339 for k, v in sorted(f.iteritems()):
342 340 if match and not match(v):
343 341 continue
344 342 # remove copies
345 343 if v in a:
346 344 continue
347 345 r[v] = k
348 346 return r
349 347
350 348 def pathcopies(x, y, match=None):
351 349 """find {dst@y: src@x} copy mapping for directed compare"""
352 350 repo = x._repo
353 351 debug = repo.ui.debugflag and repo.ui.configbool('devel', 'debug.copies')
354 352 if debug:
355 353 repo.ui.debug('debug.copies: searching copies from %s to %s\n'
356 354 % (x, y))
357 355 if x == y or not x or not y:
358 356 return {}
359 357 a = y.ancestor(x)
360 358 if a == x:
361 359 if debug:
362 360 repo.ui.debug('debug.copies: search mode: forward\n')
363 361 if y.rev() is None and x == y.p1():
364 362 # short-circuit to avoid issues with merge states
365 363 return _dirstatecopies(repo, match)
366 364 copies = _forwardcopies(x, y, match=match)
367 365 elif a == y:
368 366 if debug:
369 367 repo.ui.debug('debug.copies: search mode: backward\n')
370 368 copies = _backwardrenames(x, y, match=match)
371 369 else:
372 370 if debug:
373 371 repo.ui.debug('debug.copies: search mode: combined\n')
374 372 copies = _chain(_backwardrenames(x, a, match=match),
375 373 _forwardcopies(a, y, match=match))
376 _filter(x, y, copies)
374 _filter(x, y, copies)
377 375 return copies
378 376
379 377 def mergecopies(repo, c1, c2, base):
380 378 """
381 379 Finds moves and copies between context c1 and c2 that are relevant for
382 380 merging. 'base' will be used as the merge base.
383 381
384 382 Copytracing is used in commands like rebase, merge, unshelve, etc to merge
385 383 files that were moved/ copied in one merge parent and modified in another.
386 384 For example:
387 385
388 386 o ---> 4 another commit
389 387 |
390 388 | o ---> 3 commit that modifies a.txt
391 389 | /
392 390 o / ---> 2 commit that moves a.txt to b.txt
393 391 |/
394 392 o ---> 1 merge base
395 393
396 394 If we try to rebase revision 3 on revision 4, since there is no a.txt in
397 395 revision 4, and if user have copytrace disabled, we prints the following
398 396 message:
399 397
400 398 ```other changed <file> which local deleted```
401 399
402 400 Returns five dicts: "copy", "movewithdir", "diverge", "renamedelete" and
403 401 "dirmove".
404 402
405 403 "copy" is a mapping from destination name -> source name,
406 404 where source is in c1 and destination is in c2 or vice-versa.
407 405
408 406 "movewithdir" is a mapping from source name -> destination name,
409 407 where the file at source present in one context but not the other
410 408 needs to be moved to destination by the merge process, because the
411 409 other context moved the directory it is in.
412 410
413 411 "diverge" is a mapping of source name -> list of destination names
414 412 for divergent renames.
415 413
416 414 "renamedelete" is a mapping of source name -> list of destination
417 415 names for files deleted in c1 that were renamed in c2 or vice-versa.
418 416
419 417 "dirmove" is a mapping of detected source dir -> destination dir renames.
420 418 This is needed for handling changes to new files previously grafted into
421 419 renamed directories.
422 420
423 421 This function calls different copytracing algorithms based on config.
424 422 """
425 423 # avoid silly behavior for update from empty dir
426 424 if not c1 or not c2 or c1 == c2:
427 425 return {}, {}, {}, {}, {}
428 426
429 427 narrowmatch = c1.repo().narrowmatch()
430 428
431 429 # avoid silly behavior for parent -> working dir
432 430 if c2.node() is None and c1.node() == repo.dirstate.p1():
433 431 return _dirstatecopies(repo, narrowmatch), {}, {}, {}, {}
434 432
435 433 copytracing = repo.ui.config('experimental', 'copytrace')
436 434 if stringutil.parsebool(copytracing) is False:
437 435 # stringutil.parsebool() returns None when it is unable to parse the
438 436 # value, so we should rely on making sure copytracing is on such cases
439 437 return {}, {}, {}, {}, {}
440 438
441 439 if usechangesetcentricalgo(repo):
442 440 # The heuristics don't make sense when we need changeset-centric algos
443 441 return _fullcopytracing(repo, c1, c2, base)
444 442
445 443 # Copy trace disabling is explicitly below the node == p1 logic above
446 444 # because the logic above is required for a simple copy to be kept across a
447 445 # rebase.
448 446 if copytracing == 'heuristics':
449 447 # Do full copytracing if only non-public revisions are involved as
450 448 # that will be fast enough and will also cover the copies which could
451 449 # be missed by heuristics
452 450 if _isfullcopytraceable(repo, c1, base):
453 451 return _fullcopytracing(repo, c1, c2, base)
454 452 return _heuristicscopytracing(repo, c1, c2, base)
455 453 else:
456 454 return _fullcopytracing(repo, c1, c2, base)
457 455
458 456 def _isfullcopytraceable(repo, c1, base):
459 457 """ Checks that if base, source and destination are all no-public branches,
460 458 if yes let's use the full copytrace algorithm for increased capabilities
461 459 since it will be fast enough.
462 460
463 461 `experimental.copytrace.sourcecommitlimit` can be used to set a limit for
464 462 number of changesets from c1 to base such that if number of changesets are
465 463 more than the limit, full copytracing algorithm won't be used.
466 464 """
467 465 if c1.rev() is None:
468 466 c1 = c1.p1()
469 467 if c1.mutable() and base.mutable():
470 468 sourcecommitlimit = repo.ui.configint('experimental',
471 469 'copytrace.sourcecommitlimit')
472 470 commits = len(repo.revs('%d::%d', base.rev(), c1.rev()))
473 471 return commits < sourcecommitlimit
474 472 return False
475 473
476 474 def _checksinglesidecopies(src, dsts1, m1, m2, mb, c2, base,
477 475 copy, renamedelete):
478 476 if src not in m2:
479 477 # deleted on side 2
480 478 if src not in m1:
481 479 # renamed on side 1, deleted on side 2
482 480 renamedelete[src] = dsts1
483 481 elif m2[src] != mb[src]:
484 482 if not _related(c2[src], base[src]):
485 483 return
486 484 # modified on side 2
487 485 for dst in dsts1:
488 486 if dst not in m2:
489 487 # dst not added on side 2 (handle as regular
490 488 # "both created" case in manifestmerge otherwise)
491 489 copy[dst] = src
492 490
493 491 def _fullcopytracing(repo, c1, c2, base):
494 492 """ The full copytracing algorithm which finds all the new files that were
495 493 added from merge base up to the top commit and for each file it checks if
496 494 this file was copied from another file.
497 495
498 496 This is pretty slow when a lot of changesets are involved but will track all
499 497 the copies.
500 498 """
501 499 m1 = c1.manifest()
502 500 m2 = c2.manifest()
503 501 mb = base.manifest()
504 502
505 503 copies1 = pathcopies(base, c1)
506 504 copies2 = pathcopies(base, c2)
507 505
508 506 inversecopies1 = {}
509 507 inversecopies2 = {}
510 508 for dst, src in copies1.items():
511 509 inversecopies1.setdefault(src, []).append(dst)
512 510 for dst, src in copies2.items():
513 511 inversecopies2.setdefault(src, []).append(dst)
514 512
515 513 copy = {}
516 514 diverge = {}
517 515 renamedelete = {}
518 516 allsources = set(inversecopies1) | set(inversecopies2)
519 517 for src in allsources:
520 518 dsts1 = inversecopies1.get(src)
521 519 dsts2 = inversecopies2.get(src)
522 520 if dsts1 and dsts2:
523 521 # copied/renamed on both sides
524 522 if src not in m1 and src not in m2:
525 523 # renamed on both sides
526 524 dsts1 = set(dsts1)
527 525 dsts2 = set(dsts2)
528 526 # If there's some overlap in the rename destinations, we
529 527 # consider it not divergent. For example, if side 1 copies 'a'
530 528 # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c'
531 529 # and 'd' and deletes 'a'.
532 530 if dsts1 & dsts2:
533 531 for dst in (dsts1 & dsts2):
534 532 copy[dst] = src
535 533 else:
536 534 diverge[src] = sorted(dsts1 | dsts2)
537 535 elif src in m1 and src in m2:
538 536 # copied on both sides
539 537 dsts1 = set(dsts1)
540 538 dsts2 = set(dsts2)
541 539 for dst in (dsts1 & dsts2):
542 540 copy[dst] = src
543 541 # TODO: Handle cases where it was renamed on one side and copied
544 542 # on the other side
545 543 elif dsts1:
546 544 # copied/renamed only on side 1
547 545 _checksinglesidecopies(src, dsts1, m1, m2, mb, c2, base,
548 546 copy, renamedelete)
549 547 elif dsts2:
550 548 # copied/renamed only on side 2
551 549 _checksinglesidecopies(src, dsts2, m2, m1, mb, c1, base,
552 550 copy, renamedelete)
553 551
554 552 renamedeleteset = set()
555 553 divergeset = set()
556 554 for dsts in diverge.values():
557 555 divergeset.update(dsts)
558 556 for dsts in renamedelete.values():
559 557 renamedeleteset.update(dsts)
560 558
561 559 # find interesting file sets from manifests
562 560 addedinm1 = m1.filesnotin(mb, repo.narrowmatch())
563 561 addedinm2 = m2.filesnotin(mb, repo.narrowmatch())
564 562 u1 = sorted(addedinm1 - addedinm2)
565 563 u2 = sorted(addedinm2 - addedinm1)
566 564
567 565 header = " unmatched files in %s"
568 566 if u1:
569 567 repo.ui.debug("%s:\n %s\n" % (header % 'local', "\n ".join(u1)))
570 568 if u2:
571 569 repo.ui.debug("%s:\n %s\n" % (header % 'other', "\n ".join(u2)))
572 570
573 571 fullcopy = copies1.copy()
574 572 fullcopy.update(copies2)
575 573 if not fullcopy:
576 574 return copy, {}, diverge, renamedelete, {}
577 575
578 576 if repo.ui.debugflag:
579 577 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
580 578 "% = renamed and deleted):\n")
581 579 for f in sorted(fullcopy):
582 580 note = ""
583 581 if f in copy:
584 582 note += "*"
585 583 if f in divergeset:
586 584 note += "!"
587 585 if f in renamedeleteset:
588 586 note += "%"
589 587 repo.ui.debug(" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
590 588 note))
591 589 del divergeset
592 590
593 591 repo.ui.debug(" checking for directory renames\n")
594 592
595 593 # generate a directory move map
596 594 d1, d2 = c1.dirs(), c2.dirs()
597 595 invalid = set()
598 596 dirmove = {}
599 597
600 598 # examine each file copy for a potential directory move, which is
601 599 # when all the files in a directory are moved to a new directory
602 600 for dst, src in fullcopy.iteritems():
603 601 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
604 602 if dsrc in invalid:
605 603 # already seen to be uninteresting
606 604 continue
607 605 elif dsrc in d1 and ddst in d1:
608 606 # directory wasn't entirely moved locally
609 607 invalid.add(dsrc)
610 608 elif dsrc in d2 and ddst in d2:
611 609 # directory wasn't entirely moved remotely
612 610 invalid.add(dsrc)
613 611 elif dsrc in dirmove and dirmove[dsrc] != ddst:
614 612 # files from the same directory moved to two different places
615 613 invalid.add(dsrc)
616 614 else:
617 615 # looks good so far
618 616 dirmove[dsrc] = ddst
619 617
620 618 for i in invalid:
621 619 if i in dirmove:
622 620 del dirmove[i]
623 621 del d1, d2, invalid
624 622
625 623 if not dirmove:
626 624 return copy, {}, diverge, renamedelete, {}
627 625
628 626 dirmove = {k + "/": v + "/" for k, v in dirmove.iteritems()}
629 627
630 628 for d in dirmove:
631 629 repo.ui.debug(" discovered dir src: '%s' -> dst: '%s'\n" %
632 630 (d, dirmove[d]))
633 631
634 632 movewithdir = {}
635 633 # check unaccounted nonoverlapping files against directory moves
636 634 for f in u1 + u2:
637 635 if f not in fullcopy:
638 636 for d in dirmove:
639 637 if f.startswith(d):
640 638 # new file added in a directory that was moved, move it
641 639 df = dirmove[d] + f[len(d):]
642 640 if df not in copy:
643 641 movewithdir[f] = df
644 642 repo.ui.debug((" pending file src: '%s' -> "
645 643 "dst: '%s'\n") % (f, df))
646 644 break
647 645
648 646 return copy, movewithdir, diverge, renamedelete, dirmove
649 647
650 648 def _heuristicscopytracing(repo, c1, c2, base):
651 649 """ Fast copytracing using filename heuristics
652 650
653 651 Assumes that moves or renames are of following two types:
654 652
655 653 1) Inside a directory only (same directory name but different filenames)
656 654 2) Move from one directory to another
657 655 (same filenames but different directory names)
658 656
659 657 Works only when there are no merge commits in the "source branch".
660 658 Source branch is commits from base up to c2 not including base.
661 659
662 660 If merge is involved it fallbacks to _fullcopytracing().
663 661
664 662 Can be used by setting the following config:
665 663
666 664 [experimental]
667 665 copytrace = heuristics
668 666
669 667 In some cases the copy/move candidates found by heuristics can be very large
670 668 in number and that will make the algorithm slow. The number of possible
671 669 candidates to check can be limited by using the config
672 670 `experimental.copytrace.movecandidateslimit` which defaults to 100.
673 671 """
674 672
675 673 if c1.rev() is None:
676 674 c1 = c1.p1()
677 675 if c2.rev() is None:
678 676 c2 = c2.p1()
679 677
680 678 copies = {}
681 679
682 680 changedfiles = set()
683 681 m1 = c1.manifest()
684 682 if not repo.revs('%d::%d', base.rev(), c2.rev()):
685 683 # If base is not in c2 branch, we switch to fullcopytracing
686 684 repo.ui.debug("switching to full copytracing as base is not "
687 685 "an ancestor of c2\n")
688 686 return _fullcopytracing(repo, c1, c2, base)
689 687
690 688 ctx = c2
691 689 while ctx != base:
692 690 if len(ctx.parents()) == 2:
693 691 # To keep things simple let's not handle merges
694 692 repo.ui.debug("switching to full copytracing because of merges\n")
695 693 return _fullcopytracing(repo, c1, c2, base)
696 694 changedfiles.update(ctx.files())
697 695 ctx = ctx.p1()
698 696
699 697 cp = _forwardcopies(base, c2)
700 698 for dst, src in cp.iteritems():
701 699 if src in m1:
702 700 copies[dst] = src
703 701
704 702 # file is missing if it isn't present in the destination, but is present in
705 703 # the base and present in the source.
706 704 # Presence in the base is important to exclude added files, presence in the
707 705 # source is important to exclude removed files.
708 706 filt = lambda f: f not in m1 and f in base and f in c2
709 707 missingfiles = [f for f in changedfiles if filt(f)]
710 708
711 709 if missingfiles:
712 710 basenametofilename = collections.defaultdict(list)
713 711 dirnametofilename = collections.defaultdict(list)
714 712
715 713 for f in m1.filesnotin(base.manifest()):
716 714 basename = os.path.basename(f)
717 715 dirname = os.path.dirname(f)
718 716 basenametofilename[basename].append(f)
719 717 dirnametofilename[dirname].append(f)
720 718
721 719 for f in missingfiles:
722 720 basename = os.path.basename(f)
723 721 dirname = os.path.dirname(f)
724 722 samebasename = basenametofilename[basename]
725 723 samedirname = dirnametofilename[dirname]
726 724 movecandidates = samebasename + samedirname
727 725 # f is guaranteed to be present in c2, that's why
728 726 # c2.filectx(f) won't fail
729 727 f2 = c2.filectx(f)
730 728 # we can have a lot of candidates which can slow down the heuristics
731 729 # config value to limit the number of candidates moves to check
732 730 maxcandidates = repo.ui.configint('experimental',
733 731 'copytrace.movecandidateslimit')
734 732
735 733 if len(movecandidates) > maxcandidates:
736 734 repo.ui.status(_("skipping copytracing for '%s', more "
737 735 "candidates than the limit: %d\n")
738 736 % (f, len(movecandidates)))
739 737 continue
740 738
741 739 for candidate in movecandidates:
742 740 f1 = c1.filectx(candidate)
743 741 if _related(f1, f2):
744 742 # if there are a few related copies then we'll merge
745 743 # changes into all of them. This matches the behaviour
746 744 # of upstream copytracing
747 745 copies[candidate] = f
748 746
749 747 return copies, {}, {}, {}, {}
750 748
751 749 def _related(f1, f2):
752 750 """return True if f1 and f2 filectx have a common ancestor
753 751
754 752 Walk back to common ancestor to see if the two files originate
755 753 from the same file. Since workingfilectx's rev() is None it messes
756 754 up the integer comparison logic, hence the pre-step check for
757 755 None (f1 and f2 can only be workingfilectx's initially).
758 756 """
759 757
760 758 if f1 == f2:
761 759 return True # a match
762 760
763 761 g1, g2 = f1.ancestors(), f2.ancestors()
764 762 try:
765 763 f1r, f2r = f1.linkrev(), f2.linkrev()
766 764
767 765 if f1r is None:
768 766 f1 = next(g1)
769 767 if f2r is None:
770 768 f2 = next(g2)
771 769
772 770 while True:
773 771 f1r, f2r = f1.linkrev(), f2.linkrev()
774 772 if f1r > f2r:
775 773 f1 = next(g1)
776 774 elif f2r > f1r:
777 775 f2 = next(g2)
778 776 else: # f1 and f2 point to files in the same linkrev
779 777 return f1 == f2 # true if they point to the same file
780 778 except StopIteration:
781 779 return False
782 780
783 781 def duplicatecopies(repo, wctx, rev, fromrev, skiprev=None):
784 782 """reproduce copies from fromrev to rev in the dirstate
785 783
786 784 If skiprev is specified, it's a revision that should be used to
787 785 filter copy records. Any copies that occur between fromrev and
788 786 skiprev will not be duplicated, even if they appear in the set of
789 787 copies between fromrev and rev.
790 788 """
791 789 exclude = {}
792 790 ctraceconfig = repo.ui.config('experimental', 'copytrace')
793 791 bctrace = stringutil.parsebool(ctraceconfig)
794 792 if (skiprev is not None and
795 793 (ctraceconfig == 'heuristics' or bctrace or bctrace is None)):
796 794 # copytrace='off' skips this line, but not the entire function because
797 795 # the line below is O(size of the repo) during a rebase, while the rest
798 796 # of the function is much faster (and is required for carrying copy
799 797 # metadata across the rebase anyway).
800 798 exclude = pathcopies(repo[fromrev], repo[skiprev])
801 799 for dst, src in pathcopies(repo[fromrev], repo[rev]).iteritems():
802 800 if dst in exclude:
803 801 continue
804 802 if dst in wctx:
805 803 wctx[dst].markcopied(src)
@@ -1,387 +1,401 b''
1 1 #testcases filelog compatibility changeset
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [extensions]
5 5 > rebase=
6 6 > [alias]
7 7 > l = log -G -T '{rev} {desc}\n{files}\n'
8 8 > EOF
9 9
10 10 #if compatibility
11 11 $ cat >> $HGRCPATH << EOF
12 12 > [experimental]
13 13 > copies.read-from = compatibility
14 14 > EOF
15 15 #endif
16 16
17 17 #if changeset
18 18 $ cat >> $HGRCPATH << EOF
19 19 > [experimental]
20 20 > copies.read-from = changeset-only
21 21 > copies.write-to = changeset-only
22 22 > EOF
23 23 #endif
24 24
25 25 $ REPONUM=0
26 26 $ newrepo() {
27 27 > cd $TESTTMP
28 28 > REPONUM=`expr $REPONUM + 1`
29 29 > hg init repo-$REPONUM
30 30 > cd repo-$REPONUM
31 31 > }
32 32
33 33 Copy a file, then delete destination, then copy again. This does not create a new filelog entry.
34 34 $ newrepo
35 35 $ echo x > x
36 36 $ hg ci -Aqm 'add x'
37 37 $ echo x2 > x
38 38 $ hg ci -m 'modify x'
39 39 $ hg co -q 0
40 40 $ hg cp x y
41 41 $ hg ci -qm 'copy x to y'
42 42 $ hg rm y
43 43 $ hg ci -m 'remove y'
44 44 $ hg cp -f x y
45 45 $ hg ci -m 'copy x onto y (again)'
46 46 $ hg l
47 47 @ 4 copy x onto y (again)
48 48 | y
49 49 o 3 remove y
50 50 | y
51 51 o 2 copy x to y
52 52 | y
53 53 | o 1 modify x
54 54 |/ x
55 55 o 0 add x
56 56 x
57 57 $ hg debugp1copies -r 4
58 58 x -> y
59 59 $ hg debugpathcopies 0 4
60 60 x -> y
61 61 $ hg graft -r 1
62 62 grafting 1:* "modify x" (glob)
63 63 merging y and x to y
64 64 $ hg co -qC 1
65 65 $ hg graft -r 4
66 66 grafting 4:* "copy x onto y (again)" (glob)
67 67 merging x and y to y
68 68
69 69 Copy x to y, then remove y, then add back y. With copy metadata in the
70 70 changeset, this could easily end up reporting y as copied from x (if we don't
71 71 unmark it as a copy when it's removed). Despite x and y not being related, we
72 72 want grafts to propagate across the rename.
73 73 $ newrepo
74 74 $ echo x > x
75 75 $ hg ci -Aqm 'add x'
76 76 $ echo x2 > x
77 77 $ hg ci -m 'modify x'
78 78 $ hg co -q 0
79 79 $ hg mv x y
80 80 $ hg ci -qm 'rename x to y'
81 81 $ hg rm y
82 82 $ hg ci -qm 'remove y'
83 83 $ echo x > y
84 84 $ hg ci -Aqm 'add back y'
85 85 $ hg l
86 86 @ 4 add back y
87 87 | y
88 88 o 3 remove y
89 89 | y
90 90 o 2 rename x to y
91 91 | x y
92 92 | o 1 modify x
93 93 |/ x
94 94 o 0 add x
95 95 x
96 96 $ hg debugpathcopies 0 4
97 97 BROKEN: This should succeed and merge the changes from x into y
98 98 $ hg graft -r 1
99 99 grafting 1:* "modify x" (glob)
100 100 file 'x' was deleted in local [local] but was modified in other [graft].
101 101 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
102 102 What do you want to do? u
103 103 abort: unresolved conflicts, can't continue
104 104 (use 'hg resolve' and 'hg graft --continue')
105 105 [255]
106 106
107 107 Add x, remove it, then add it back, then rename x to y. Similar to the case
108 108 above, but here the break in history is before the rename.
109 109 $ newrepo
110 110 $ echo x > x
111 111 $ hg ci -Aqm 'add x'
112 112 $ echo x2 > x
113 113 $ hg ci -m 'modify x'
114 114 $ hg co -q 0
115 115 $ hg rm x
116 116 $ hg ci -qm 'remove x'
117 117 $ echo x > x
118 118 $ hg ci -Aqm 'add x again'
119 119 $ hg mv x y
120 120 $ hg ci -m 'rename x to y'
121 121 $ hg l
122 122 @ 4 rename x to y
123 123 | x y
124 124 o 3 add x again
125 125 | x
126 126 o 2 remove x
127 127 | x
128 128 | o 1 modify x
129 129 |/ x
130 130 o 0 add x
131 131 x
132 132 $ hg debugpathcopies 0 4
133 133 x -> y
134 134 $ hg graft -r 1
135 135 grafting 1:* "modify x" (glob)
136 136 merging y and x to y
137 137 $ hg co -qC 1
138 138 $ hg graft -r 4
139 139 grafting 4:* "rename x to y" (glob)
140 140 merging x and y to y
141 141
142 142 Add x, modify it, remove it, then add it back, then rename x to y. Similar to
143 143 the case above, but here the re-added file's nodeid is different from before
144 144 the break.
145 145
146 146 $ newrepo
147 147 $ echo x > x
148 148 $ hg ci -Aqm 'add x'
149 149 $ echo x2 > x
150 150 $ hg ci -m 'modify x'
151 151 $ echo x3 > x
152 152 $ hg ci -qm 'modify x again'
153 153 $ hg co -q 1
154 154 $ hg rm x
155 155 $ hg ci -qm 'remove x'
156 156 # Same content to avoid conflicts
157 157 $ hg revert -r 1 x
158 158 $ hg ci -Aqm 'add x again'
159 159 $ hg mv x y
160 160 $ hg ci -m 'rename x to y'
161 161 $ hg l
162 162 @ 5 rename x to y
163 163 | x y
164 164 o 4 add x again
165 165 | x
166 166 o 3 remove x
167 167 | x
168 168 | o 2 modify x again
169 169 |/ x
170 170 o 1 modify x
171 171 | x
172 172 o 0 add x
173 173 x
174 174 $ hg debugpathcopies 0 5
175 175 x -> y (no-filelog !)
176 176 #if no-filelog
177 177 $ hg graft -r 2
178 178 grafting 2:* "modify x again" (glob)
179 179 merging y and x to y
180 180 #else
181 181 BROKEN: This should succeed and merge the changes from x into y
182 182 $ hg graft -r 2
183 183 grafting 2:* "modify x again" (glob)
184 184 file 'x' was deleted in local [local] but was modified in other [graft].
185 185 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
186 186 What do you want to do? u
187 187 abort: unresolved conflicts, can't continue
188 188 (use 'hg resolve' and 'hg graft --continue')
189 189 [255]
190 190 #endif
191 191 $ hg co -qC 2
192 192 BROKEN: This should succeed and merge the changes from x into y
193 193 $ hg graft -r 5
194 194 grafting 5:* "rename x to y"* (glob)
195 195 file 'x' was deleted in other [graft] but was modified in local [local].
196 196 You can use (c)hanged version, (d)elete, or leave (u)nresolved.
197 197 What do you want to do? u
198 198 abort: unresolved conflicts, can't continue
199 199 (use 'hg resolve' and 'hg graft --continue')
200 200 [255]
201 201
202 202 Add x, remove it, then add it back, rename x to y from the first commit.
203 203 Similar to the case above, but here the break in history is parallel to the
204 204 rename.
205 205 $ newrepo
206 206 $ echo x > x
207 207 $ hg ci -Aqm 'add x'
208 208 $ hg rm x
209 209 $ hg ci -qm 'remove x'
210 210 $ echo x > x
211 211 $ hg ci -Aqm 'add x again'
212 212 $ echo x2 > x
213 213 $ hg ci -m 'modify x'
214 214 $ hg co -q 0
215 215 $ hg mv x y
216 216 $ hg ci -qm 'rename x to y'
217 217 $ hg l
218 218 @ 4 rename x to y
219 219 | x y
220 220 | o 3 modify x
221 221 | | x
222 222 | o 2 add x again
223 223 | | x
224 224 | o 1 remove x
225 225 |/ x
226 226 o 0 add x
227 227 x
228 228 $ hg debugpathcopies 2 4
229 229 x -> y
230 230 $ hg graft -r 3
231 231 grafting 3:* "modify x" (glob)
232 232 merging y and x to y
233 233 $ hg co -qC 3
234 234 $ hg graft -r 4
235 235 grafting 4:* "rename x to y" (glob)
236 236 merging x and y to y
237 237
238 238 Add x, remove it, then add it back, rename x to y from the first commit.
239 239 Similar to the case above, but here the re-added file's nodeid is different
240 240 from the base.
241 241 $ newrepo
242 242 $ echo x > x
243 243 $ hg ci -Aqm 'add x'
244 244 $ hg rm x
245 245 $ hg ci -qm 'remove x'
246 246 $ echo x2 > x
247 247 $ hg ci -Aqm 'add x again with different content'
248 248 $ hg co -q 0
249 249 $ hg mv x y
250 250 $ hg ci -qm 'rename x to y'
251 251 $ hg l
252 252 @ 3 rename x to y
253 253 | x y
254 254 | o 2 add x again with different content
255 255 | | x
256 256 | o 1 remove x
257 257 |/ x
258 258 o 0 add x
259 259 x
260 260 $ hg debugpathcopies 2 3
261 261 x -> y
262 262 BROKEN: This should merge the changes from x into y
263 263 $ hg graft -r 2
264 264 grafting 2:* "add x again with different content" (glob)
265 265 $ hg co -qC 2
266 266 BROKEN: This should succeed and merge the changes from x into y
267 267 $ hg graft -r 3
268 268 grafting 3:* "rename x to y" (glob)
269 269 file 'x' was deleted in other [graft] but was modified in local [local].
270 270 You can use (c)hanged version, (d)elete, or leave (u)nresolved.
271 271 What do you want to do? u
272 272 abort: unresolved conflicts, can't continue
273 273 (use 'hg resolve' and 'hg graft --continue')
274 274 [255]
275 275
276 276 Add x on two branches, then rename x to y on one side. Similar to the case
277 277 above, but here the break in history is via the base commit.
278 278 $ newrepo
279 279 $ echo a > a
280 280 $ hg ci -Aqm 'base'
281 281 $ echo x > x
282 282 $ hg ci -Aqm 'add x'
283 283 $ echo x2 > x
284 284 $ hg ci -m 'modify x'
285 285 $ hg co -q 0
286 286 $ echo x > x
287 287 $ hg ci -Aqm 'add x again'
288 288 $ hg mv x y
289 289 $ hg ci -qm 'rename x to y'
290 290 $ hg l
291 291 @ 4 rename x to y
292 292 | x y
293 293 o 3 add x again
294 294 | x
295 295 | o 2 modify x
296 296 | | x
297 297 | o 1 add x
298 298 |/ x
299 299 o 0 base
300 300 a
301 301 $ hg debugpathcopies 1 4
302 x -> y (no-filelog !)
303 #if filelog
302 304 BROKEN: This should succeed and merge the changes from x into y
303 305 $ hg graft -r 2
304 306 grafting 2:* "modify x" (glob)
305 307 file 'x' was deleted in local [local] but was modified in other [graft].
306 308 What do you want to do?
307 309 use (c)hanged version, leave (d)eleted, or leave (u)nresolved? u
308 310 abort: unresolved conflicts, can't continue
309 311 (use 'hg resolve' and 'hg graft --continue')
310 312 [255]
313 #else
314 $ hg graft -r 2
315 grafting 2:* "modify x" (glob)
316 merging y and x to y
317 #endif
311 318 $ hg co -qC 2
312 319 $ hg graft -r 4
313 320 grafting 4:* "rename x to y"* (glob)
314 321 merging x and y to y
315 322
316 323 Add x on two branches, with same content but different history, then rename x
317 324 to y on one side. Similar to the case above, here the file's nodeid is
318 325 different between the branches.
319 326 $ newrepo
320 327 $ echo a > a
321 328 $ hg ci -Aqm 'base'
322 329 $ echo x > x
323 330 $ hg ci -Aqm 'add x'
324 331 $ echo x2 > x
325 332 $ hg ci -m 'modify x'
326 333 $ hg co -q 0
327 334 $ touch x
328 335 $ hg ci -Aqm 'add empty x'
329 336 # Same content to avoid conflicts
330 337 $ hg revert -r 1 x
331 338 $ hg ci -m 'modify x to match commit 1'
332 339 $ hg mv x y
333 340 $ hg ci -qm 'rename x to y'
334 341 $ hg l
335 342 @ 5 rename x to y
336 343 | x y
337 344 o 4 modify x to match commit 1
338 345 | x
339 346 o 3 add empty x
340 347 | x
341 348 | o 2 modify x
342 349 | | x
343 350 | o 1 add x
344 351 |/ x
345 352 o 0 base
346 353 a
347 354 $ hg debugpathcopies 1 5
355 x -> y (no-filelog !)
356 #if filelog
348 357 BROKEN: This should succeed and merge the changes from x into y
349 358 $ hg graft -r 2
350 359 grafting 2:* "modify x" (glob)
351 360 file 'x' was deleted in local [local] but was modified in other [graft].
352 361 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
353 362 What do you want to do? u
354 363 abort: unresolved conflicts, can't continue
355 364 (use 'hg resolve' and 'hg graft --continue')
356 365 [255]
366 #else
367 $ hg graft -r 2
368 grafting 2:* "modify x" (glob)
369 merging y and x to y
370 #endif
357 371 $ hg co -qC 2
358 372 BROKEN: This should succeed and merge the changes from x into y
359 373 $ hg graft -r 5
360 374 grafting 5:* "rename x to y"* (glob)
361 375 file 'x' was deleted in other [graft] but was modified in local [local].
362 376 You can use (c)hanged version, (d)elete, or leave (u)nresolved.
363 377 What do you want to do? u
364 378 abort: unresolved conflicts, can't continue
365 379 (use 'hg resolve' and 'hg graft --continue')
366 380 [255]
367 381
368 382 Copies via null revision (there shouldn't be any)
369 383 $ newrepo
370 384 $ echo x > x
371 385 $ hg ci -Aqm 'add x'
372 386 $ hg cp x y
373 387 $ hg ci -m 'copy x to y'
374 388 $ hg co -q null
375 389 $ echo x > x
376 390 $ hg ci -Aqm 'add x (again)'
377 391 $ hg l
378 392 @ 2 add x (again)
379 393 x
380 394 o 1 copy x to y
381 395 | y
382 396 o 0 add x
383 397 x
384 398 $ hg debugpathcopies 1 2
385 399 $ hg debugpathcopies 2 1
386 400 $ hg graft -r 1
387 401 grafting 1:* "copy x to y" (glob)
@@ -1,587 +1,587 b''
1 1 #testcases filelog compatibility changeset
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [extensions]
5 5 > rebase=
6 6 > [alias]
7 7 > l = log -G -T '{rev} {desc}\n{files}\n'
8 8 > EOF
9 9
10 10 #if compatibility
11 11 $ cat >> $HGRCPATH << EOF
12 12 > [experimental]
13 13 > copies.read-from = compatibility
14 14 > EOF
15 15 #endif
16 16
17 17 #if changeset
18 18 $ cat >> $HGRCPATH << EOF
19 19 > [experimental]
20 20 > copies.read-from = changeset-only
21 21 > copies.write-to = changeset-only
22 22 > EOF
23 23 #endif
24 24
25 25 $ REPONUM=0
26 26 $ newrepo() {
27 27 > cd $TESTTMP
28 28 > REPONUM=`expr $REPONUM + 1`
29 29 > hg init repo-$REPONUM
30 30 > cd repo-$REPONUM
31 31 > }
32 32
33 33 Simple rename case
34 34 $ newrepo
35 35 $ echo x > x
36 36 $ hg ci -Aqm 'add x'
37 37 $ hg mv x y
38 38 $ hg debugp1copies
39 39 x -> y
40 40 $ hg debugp2copies
41 41 $ hg ci -m 'rename x to y'
42 42 $ hg l
43 43 @ 1 rename x to y
44 44 | x y
45 45 o 0 add x
46 46 x
47 47 $ hg debugp1copies -r 1
48 48 x -> y
49 49 $ hg debugpathcopies 0 1
50 50 x -> y
51 51 $ hg debugpathcopies 1 0
52 52 y -> x
53 53 Test filtering copies by path. We do filtering by destination.
54 54 $ hg debugpathcopies 0 1 x
55 55 $ hg debugpathcopies 1 0 x
56 56 y -> x
57 57 $ hg debugpathcopies 0 1 y
58 58 x -> y
59 59 $ hg debugpathcopies 1 0 y
60 60
61 61 Copy a file onto another file
62 62 $ newrepo
63 63 $ echo x > x
64 64 $ echo y > y
65 65 $ hg ci -Aqm 'add x and y'
66 66 $ hg cp -f x y
67 67 $ hg debugp1copies
68 68 x -> y
69 69 $ hg debugp2copies
70 70 $ hg ci -m 'copy x onto y'
71 71 $ hg l
72 72 @ 1 copy x onto y
73 73 | y
74 74 o 0 add x and y
75 75 x y
76 76 $ hg debugp1copies -r 1
77 77 x -> y
78 78 Incorrectly doesn't show the rename
79 79 $ hg debugpathcopies 0 1
80 80
81 81 Copy a file onto another file with same content. If metadata is stored in changeset, this does not
82 82 produce a new filelog entry. The changeset's "files" entry should still list the file.
83 83 $ newrepo
84 84 $ echo x > x
85 85 $ echo x > x2
86 86 $ hg ci -Aqm 'add x and x2 with same content'
87 87 $ hg cp -f x x2
88 88 $ hg ci -m 'copy x onto x2'
89 89 $ hg l
90 90 @ 1 copy x onto x2
91 91 | x2
92 92 o 0 add x and x2 with same content
93 93 x x2
94 94 $ hg debugp1copies -r 1
95 95 x -> x2
96 96 Incorrectly doesn't show the rename
97 97 $ hg debugpathcopies 0 1
98 98
99 99 Rename file in a loop: x->y->z->x
100 100 $ newrepo
101 101 $ echo x > x
102 102 $ hg ci -Aqm 'add x'
103 103 $ hg mv x y
104 104 $ hg debugp1copies
105 105 x -> y
106 106 $ hg debugp2copies
107 107 $ hg ci -m 'rename x to y'
108 108 $ hg mv y z
109 109 $ hg ci -m 'rename y to z'
110 110 $ hg mv z x
111 111 $ hg ci -m 'rename z to x'
112 112 $ hg l
113 113 @ 3 rename z to x
114 114 | x z
115 115 o 2 rename y to z
116 116 | y z
117 117 o 1 rename x to y
118 118 | x y
119 119 o 0 add x
120 120 x
121 121 $ hg debugpathcopies 0 3
122 122
123 123 Copy x to z, then remove z, then copy x2 (same content as x) to z. With copy metadata in the
124 124 changeset, the two copies here will have the same filelog entry, so ctx['z'].introrev() might point
125 125 to the first commit that added the file. We should still report the copy as being from x2.
126 126 $ newrepo
127 127 $ echo x > x
128 128 $ echo x > x2
129 129 $ hg ci -Aqm 'add x and x2 with same content'
130 130 $ hg cp x z
131 131 $ hg ci -qm 'copy x to z'
132 132 $ hg rm z
133 133 $ hg ci -m 'remove z'
134 134 $ hg cp x2 z
135 135 $ hg ci -m 'copy x2 to z'
136 136 $ hg l
137 137 @ 3 copy x2 to z
138 138 | z
139 139 o 2 remove z
140 140 | z
141 141 o 1 copy x to z
142 142 | z
143 143 o 0 add x and x2 with same content
144 144 x x2
145 145 $ hg debugp1copies -r 3
146 146 x2 -> z
147 147 $ hg debugpathcopies 0 3
148 148 x2 -> z
149 149
150 150 Create x and y, then rename them both to the same name, but on different sides of a fork
151 151 $ newrepo
152 152 $ echo x > x
153 153 $ echo y > y
154 154 $ hg ci -Aqm 'add x and y'
155 155 $ hg mv x z
156 156 $ hg ci -qm 'rename x to z'
157 157 $ hg co -q 0
158 158 $ hg mv y z
159 159 $ hg ci -qm 'rename y to z'
160 160 $ hg l
161 161 @ 2 rename y to z
162 162 | y z
163 163 | o 1 rename x to z
164 164 |/ x z
165 165 o 0 add x and y
166 166 x y
167 167 $ hg debugpathcopies 1 2
168 168 z -> x
169 169 y -> z
170 170
171 171 Fork renames x to y on one side and removes x on the other
172 172 $ newrepo
173 173 $ echo x > x
174 174 $ hg ci -Aqm 'add x'
175 175 $ hg mv x y
176 176 $ hg ci -m 'rename x to y'
177 177 $ hg co -q 0
178 178 $ hg rm x
179 179 $ hg ci -m 'remove x'
180 180 created new head
181 181 $ hg l
182 182 @ 2 remove x
183 183 | x
184 184 | o 1 rename x to y
185 185 |/ x y
186 186 o 0 add x
187 187 x
188 188 $ hg debugpathcopies 1 2
189 189
190 190 Merge rename from other branch
191 191 $ newrepo
192 192 $ echo x > x
193 193 $ hg ci -Aqm 'add x'
194 194 $ hg mv x y
195 195 $ hg ci -m 'rename x to y'
196 196 $ hg co -q 0
197 197 $ echo z > z
198 198 $ hg ci -Aqm 'add z'
199 199 $ hg merge -q 1
200 200 $ hg debugp1copies
201 201 $ hg debugp2copies
202 202 $ hg ci -m 'merge rename from p2'
203 203 $ hg l
204 204 @ 3 merge rename from p2
205 205 |\ x
206 206 | o 2 add z
207 207 | | z
208 208 o | 1 rename x to y
209 209 |/ x y
210 210 o 0 add x
211 211 x
212 212 Perhaps we should indicate the rename here, but `hg status` is documented to be weird during
213 213 merges, so...
214 214 $ hg debugp1copies -r 3
215 215 $ hg debugp2copies -r 3
216 216 $ hg debugpathcopies 0 3
217 217 x -> y
218 218 $ hg debugpathcopies 1 2
219 219 y -> x
220 220 $ hg debugpathcopies 1 3
221 221 $ hg debugpathcopies 2 3
222 222 x -> y
223 223
224 224 Copy file from either side in a merge
225 225 $ newrepo
226 226 $ echo x > x
227 227 $ hg ci -Aqm 'add x'
228 228 $ hg co -q null
229 229 $ echo y > y
230 230 $ hg ci -Aqm 'add y'
231 231 $ hg merge -q 0
232 232 $ hg cp y z
233 233 $ hg debugp1copies
234 234 y -> z
235 235 $ hg debugp2copies
236 236 $ hg ci -m 'copy file from p1 in merge'
237 237 $ hg co -q 1
238 238 $ hg merge -q 0
239 239 $ hg cp x z
240 240 $ hg debugp1copies
241 241 $ hg debugp2copies
242 242 x -> z
243 243 $ hg ci -qm 'copy file from p2 in merge'
244 244 $ hg l
245 245 @ 3 copy file from p2 in merge
246 246 |\ z
247 247 +---o 2 copy file from p1 in merge
248 248 | |/ z
249 249 | o 1 add y
250 250 | y
251 251 o 0 add x
252 252 x
253 253 $ hg debugp1copies -r 2
254 254 y -> z
255 255 $ hg debugp2copies -r 2
256 256 $ hg debugpathcopies 1 2
257 257 y -> z
258 258 $ hg debugpathcopies 0 2
259 259 $ hg debugp1copies -r 3
260 260 $ hg debugp2copies -r 3
261 261 x -> z
262 262 $ hg debugpathcopies 1 3
263 263 $ hg debugpathcopies 0 3
264 264 x -> z
265 265
266 266 Copy file that exists on both sides of the merge, same content on both sides
267 267 $ newrepo
268 268 $ echo x > x
269 269 $ hg ci -Aqm 'add x on branch 1'
270 270 $ hg co -q null
271 271 $ echo x > x
272 272 $ hg ci -Aqm 'add x on branch 2'
273 273 $ hg merge -q 0
274 274 $ hg cp x z
275 275 $ hg debugp1copies
276 276 x -> z
277 277 $ hg debugp2copies
278 278 $ hg ci -qm 'merge'
279 279 $ hg l
280 280 @ 2 merge
281 281 |\ z
282 282 | o 1 add x on branch 2
283 283 | x
284 284 o 0 add x on branch 1
285 285 x
286 286 $ hg debugp1copies -r 2
287 287 x -> z
288 288 $ hg debugp2copies -r 2
289 289 It's a little weird that it shows up on both sides
290 290 $ hg debugpathcopies 1 2
291 291 x -> z
292 292 $ hg debugpathcopies 0 2
293 293 x -> z (filelog !)
294 294
295 295 Copy file that exists on both sides of the merge, different content
296 296 $ newrepo
297 297 $ echo branch1 > x
298 298 $ hg ci -Aqm 'add x on branch 1'
299 299 $ hg co -q null
300 300 $ echo branch2 > x
301 301 $ hg ci -Aqm 'add x on branch 2'
302 302 $ hg merge -q 0
303 303 warning: conflicts while merging x! (edit, then use 'hg resolve --mark')
304 304 [1]
305 305 $ echo resolved > x
306 306 $ hg resolve -m x
307 307 (no more unresolved files)
308 308 $ hg cp x z
309 309 $ hg debugp1copies
310 310 x -> z
311 311 $ hg debugp2copies
312 312 $ hg ci -qm 'merge'
313 313 $ hg l
314 314 @ 2 merge
315 315 |\ x z
316 316 | o 1 add x on branch 2
317 317 | x
318 318 o 0 add x on branch 1
319 319 x
320 320 $ hg debugp1copies -r 2
321 321 x -> z (changeset !)
322 322 $ hg debugp2copies -r 2
323 323 x -> z (no-changeset !)
324 324 $ hg debugpathcopies 1 2
325 325 x -> z (changeset !)
326 326 $ hg debugpathcopies 0 2
327 327 x -> z (no-changeset !)
328 328
329 329 Copy x->y on one side of merge and copy x->z on the other side. Pathcopies from one parent
330 330 of the merge to the merge should include the copy from the other side.
331 331 $ newrepo
332 332 $ echo x > x
333 333 $ hg ci -Aqm 'add x'
334 334 $ hg cp x y
335 335 $ hg ci -qm 'copy x to y'
336 336 $ hg co -q 0
337 337 $ hg cp x z
338 338 $ hg ci -qm 'copy x to z'
339 339 $ hg merge -q 1
340 340 $ hg ci -m 'merge copy x->y and copy x->z'
341 341 $ hg l
342 342 @ 3 merge copy x->y and copy x->z
343 343 |\
344 344 | o 2 copy x to z
345 345 | | z
346 346 o | 1 copy x to y
347 347 |/ y
348 348 o 0 add x
349 349 x
350 350 $ hg debugp1copies -r 3
351 351 $ hg debugp2copies -r 3
352 352 $ hg debugpathcopies 2 3
353 353 x -> y
354 354 $ hg debugpathcopies 1 3
355 355 x -> z
356 356
357 Copy x to y on one side of merge, create y and rename to z on the other side. Pathcopies from the
358 first side should not include the y->z rename since y didn't exist in the merge base.
357 Copy x to y on one side of merge, create y and rename to z on the other side.
359 358 $ newrepo
360 359 $ echo x > x
361 360 $ hg ci -Aqm 'add x'
362 361 $ hg cp x y
363 362 $ hg ci -qm 'copy x to y'
364 363 $ hg co -q 0
365 364 $ echo y > y
366 365 $ hg ci -Aqm 'add y'
367 366 $ hg mv y z
368 367 $ hg ci -m 'rename y to z'
369 368 $ hg merge -q 1
370 369 $ hg ci -m 'merge'
371 370 $ hg l
372 371 @ 4 merge
373 372 |\
374 373 | o 3 rename y to z
375 374 | | y z
376 375 | o 2 add y
377 376 | | y
378 377 o | 1 copy x to y
379 378 |/ y
380 379 o 0 add x
381 380 x
382 381 $ hg debugp1copies -r 3
383 382 y -> z
384 383 $ hg debugp2copies -r 3
385 384 $ hg debugpathcopies 2 3
386 385 y -> z
387 386 $ hg debugpathcopies 1 3
387 y -> z (no-filelog !)
388 388
389 389 Create x and y, then rename x to z on one side of merge, and rename y to z and
390 390 modify z on the other side. When storing copies in the changeset, we don't
391 391 filter out copies whose target was created on the other side of the merge.
392 392 $ newrepo
393 393 $ echo x > x
394 394 $ echo y > y
395 395 $ hg ci -Aqm 'add x and y'
396 396 $ hg mv x z
397 397 $ hg ci -qm 'rename x to z'
398 398 $ hg co -q 0
399 399 $ hg mv y z
400 400 $ hg ci -qm 'rename y to z'
401 401 $ echo z >> z
402 402 $ hg ci -m 'modify z'
403 403 $ hg merge -q 1
404 404 warning: conflicts while merging z! (edit, then use 'hg resolve --mark')
405 405 [1]
406 406 $ echo z > z
407 407 $ hg resolve -qm z
408 408 $ hg ci -m 'merge 1 into 3'
409 409 Try merging the other direction too
410 410 $ hg co -q 1
411 411 $ hg merge -q 3
412 412 warning: conflicts while merging z! (edit, then use 'hg resolve --mark')
413 413 [1]
414 414 $ echo z > z
415 415 $ hg resolve -qm z
416 416 $ hg ci -m 'merge 3 into 1'
417 417 created new head
418 418 $ hg l
419 419 @ 5 merge 3 into 1
420 420 |\ y z
421 421 +---o 4 merge 1 into 3
422 422 | |/ x z
423 423 | o 3 modify z
424 424 | | z
425 425 | o 2 rename y to z
426 426 | | y z
427 427 o | 1 rename x to z
428 428 |/ x z
429 429 o 0 add x and y
430 430 x y
431 431 $ hg debugpathcopies 1 4
432 432 y -> z (no-filelog !)
433 433 $ hg debugpathcopies 2 4
434 434 x -> z (no-filelog !)
435 435 $ hg debugpathcopies 0 4
436 436 x -> z (filelog !)
437 437 y -> z (compatibility !)
438 438 y -> z (changeset !)
439 439 $ hg debugpathcopies 1 5
440 440 y -> z (no-filelog !)
441 441 $ hg debugpathcopies 2 5
442 442 x -> z (no-filelog !)
443 443 $ hg debugpathcopies 0 5
444 444 x -> z
445 445
446 446
447 447 Test for a case in fullcopytracing algorithm where neither of the merging csets
448 448 is a descendant of the merge base. This test reflects that the algorithm
449 449 correctly finds the copies:
450 450
451 451 $ cat >> $HGRCPATH << EOF
452 452 > [experimental]
453 453 > evolution.createmarkers=True
454 454 > evolution.allowunstable=True
455 455 > EOF
456 456
457 457 $ newrepo
458 458 $ echo a > a
459 459 $ hg add a
460 460 $ hg ci -m "added a"
461 461 $ echo b > b
462 462 $ hg add b
463 463 $ hg ci -m "added b"
464 464
465 465 $ hg mv b b1
466 466 $ hg ci -m "rename b to b1"
467 467
468 468 $ hg up ".^"
469 469 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
470 470 $ echo d > d
471 471 $ hg add d
472 472 $ hg ci -m "added d"
473 473 created new head
474 474
475 475 $ echo baba >> b
476 476 $ hg ci --amend -m "added d, modified b"
477 477
478 478 $ hg l --hidden
479 479 @ 4 added d, modified b
480 480 | b d
481 481 | x 3 added d
482 482 |/ d
483 483 | o 2 rename b to b1
484 484 |/ b b1
485 485 o 1 added b
486 486 | b
487 487 o 0 added a
488 488 a
489 489
490 490 Grafting revision 4 on top of revision 2, showing that it respect the rename:
491 491
492 492 $ hg up 2 -q
493 493 $ hg graft -r 4 --base 3 --hidden
494 494 grafting 4:af28412ec03c "added d, modified b" (tip) (no-changeset !)
495 495 grafting 4:6325ca0b7a1c "added d, modified b" (tip) (changeset !)
496 496 merging b1 and b to b1
497 497
498 498 $ hg l -l1 -p
499 499 @ 5 added d, modified b
500 500 | b1
501 501 ~ diff -r 5a4825cc2926 -r 94a2f1a0e8e2 b1 (no-changeset !)
502 502 ~ diff -r 0a0ed3b3251c -r d544fb655520 b1 (changeset !)
503 503 --- a/b1 Thu Jan 01 00:00:00 1970 +0000
504 504 +++ b/b1 Thu Jan 01 00:00:00 1970 +0000
505 505 @@ -1,1 +1,2 @@
506 506 b
507 507 +baba
508 508
509 509 Test to make sure that fullcopytracing algorithm doesn't fail when neither of the
510 510 merging csets is a descendant of the base.
511 511 -------------------------------------------------------------------------------------------------
512 512
513 513 $ newrepo
514 514 $ echo a > a
515 515 $ hg add a
516 516 $ hg ci -m "added a"
517 517 $ echo b > b
518 518 $ hg add b
519 519 $ hg ci -m "added b"
520 520
521 521 $ echo foobar > willconflict
522 522 $ hg add willconflict
523 523 $ hg ci -m "added willconflict"
524 524 $ echo c > c
525 525 $ hg add c
526 526 $ hg ci -m "added c"
527 527
528 528 $ hg l
529 529 @ 3 added c
530 530 | c
531 531 o 2 added willconflict
532 532 | willconflict
533 533 o 1 added b
534 534 | b
535 535 o 0 added a
536 536 a
537 537
538 538 $ hg up ".^^"
539 539 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
540 540 $ echo d > d
541 541 $ hg add d
542 542 $ hg ci -m "added d"
543 543 created new head
544 544
545 545 $ echo barfoo > willconflict
546 546 $ hg add willconflict
547 547 $ hg ci --amend -m "added willconflict and d"
548 548
549 549 $ hg l
550 550 @ 5 added willconflict and d
551 551 | d willconflict
552 552 | o 3 added c
553 553 | | c
554 554 | o 2 added willconflict
555 555 |/ willconflict
556 556 o 1 added b
557 557 | b
558 558 o 0 added a
559 559 a
560 560
561 561 $ hg rebase -r . -d 2 -t :other
562 562 rebasing 5:5018b1509e94 "added willconflict and d" (tip) (no-changeset !)
563 563 rebasing 5:af8d273bf580 "added willconflict and d" (tip) (changeset !)
564 564
565 565 $ hg up 3 -q
566 566 $ hg l --hidden
567 567 o 6 added willconflict and d
568 568 | d willconflict
569 569 | x 5 added willconflict and d
570 570 | | d willconflict
571 571 | | x 4 added d
572 572 | |/ d
573 573 +---@ 3 added c
574 574 | | c
575 575 o | 2 added willconflict
576 576 |/ willconflict
577 577 o 1 added b
578 578 | b
579 579 o 0 added a
580 580 a
581 581
582 582 Now if we trigger a merge between revision 3 and 6 using base revision 4,
583 583 neither of the merging csets will be a descendant of the base revision:
584 584
585 585 $ hg graft -r 6 --base 4 --hidden -t :other
586 586 grafting 6:99802e4f1e46 "added willconflict and d" (tip) (no-changeset !)
587 587 grafting 6:b19f0df72728 "added willconflict and d" (tip) (changeset !)
General Comments 0
You need to be logged in to leave comments. Login now