##// END OF EJS Templates
copies: fix the changeset based algorithm regarding merge...
marmoute -
r45252:45f3f35c default
parent child Browse files
Show More
@@ -1,1171 +1,1241 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 multiprocessing
12 12 import os
13 13
14 14 from .i18n import _
15 15
16 16
17 17 from .revlogutils.flagutil import REVIDX_SIDEDATA
18 18
19 19 from . import (
20 20 error,
21 21 match as matchmod,
22 22 node,
23 23 pathutil,
24 24 pycompat,
25 25 util,
26 26 )
27 27
28 28 from .revlogutils import sidedata as sidedatamod
29 29
30 30 from .utils import stringutil
31 31
32 32
33 33 def _filter(src, dst, t):
34 34 """filters out invalid copies after chaining"""
35 35
36 36 # When _chain()'ing copies in 'a' (from 'src' via some other commit 'mid')
37 37 # with copies in 'b' (from 'mid' to 'dst'), we can get the different cases
38 38 # in the following table (not including trivial cases). For example, case 2
39 39 # is where a file existed in 'src' and remained under that name in 'mid' and
40 40 # then was renamed between 'mid' and 'dst'.
41 41 #
42 42 # case src mid dst result
43 43 # 1 x y - -
44 44 # 2 x y y x->y
45 45 # 3 x y x -
46 46 # 4 x y z x->z
47 47 # 5 - x y -
48 48 # 6 x x y x->y
49 49 #
50 50 # _chain() takes care of chaining the copies in 'a' and 'b', but it
51 51 # cannot tell the difference between cases 1 and 2, between 3 and 4, or
52 52 # between 5 and 6, so it includes all cases in its result.
53 53 # Cases 1, 3, and 5 are then removed by _filter().
54 54
55 55 for k, v in list(t.items()):
56 56 # remove copies from files that didn't exist
57 57 if v not in src:
58 58 del t[k]
59 59 # remove criss-crossed copies
60 60 elif k in src and v in dst:
61 61 del t[k]
62 62 # remove copies to files that were then removed
63 63 elif k not in dst:
64 64 del t[k]
65 65
66 66
67 67 def _chain(prefix, suffix):
68 68 """chain two sets of copies 'prefix' and 'suffix'"""
69 69 result = prefix.copy()
70 70 for key, value in pycompat.iteritems(suffix):
71 71 result[key] = prefix.get(value, value)
72 72 return result
73 73
74 74
75 75 def _tracefile(fctx, am, basemf):
76 76 """return file context that is the ancestor of fctx present in ancestor
77 77 manifest am
78 78
79 79 Note: we used to try and stop after a given limit, however checking if that
80 80 limit is reached turned out to be very expensive. we are better off
81 81 disabling that feature."""
82 82
83 83 for f in fctx.ancestors():
84 84 path = f.path()
85 85 if am.get(path, None) == f.filenode():
86 86 return path
87 87 if basemf and basemf.get(path, None) == f.filenode():
88 88 return path
89 89
90 90
91 91 def _dirstatecopies(repo, match=None):
92 92 ds = repo.dirstate
93 93 c = ds.copies().copy()
94 94 for k in list(c):
95 95 if ds[k] not in b'anm' or (match and not match(k)):
96 96 del c[k]
97 97 return c
98 98
99 99
100 100 def _computeforwardmissing(a, b, match=None):
101 101 """Computes which files are in b but not a.
102 102 This is its own function so extensions can easily wrap this call to see what
103 103 files _forwardcopies is about to process.
104 104 """
105 105 ma = a.manifest()
106 106 mb = b.manifest()
107 107 return mb.filesnotin(ma, match=match)
108 108
109 109
110 110 def usechangesetcentricalgo(repo):
111 111 """Checks if we should use changeset-centric copy algorithms"""
112 112 if repo.filecopiesmode == b'changeset-sidedata':
113 113 return True
114 114 readfrom = repo.ui.config(b'experimental', b'copies.read-from')
115 115 changesetsource = (b'changeset-only', b'compatibility')
116 116 return readfrom in changesetsource
117 117
118 118
119 119 def _committedforwardcopies(a, b, base, match):
120 120 """Like _forwardcopies(), but b.rev() cannot be None (working copy)"""
121 121 # files might have to be traced back to the fctx parent of the last
122 122 # one-side-only changeset, but not further back than that
123 123 repo = a._repo
124 124
125 125 if usechangesetcentricalgo(repo):
126 126 return _changesetforwardcopies(a, b, match)
127 127
128 128 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
129 129 dbg = repo.ui.debug
130 130 if debug:
131 131 dbg(b'debug.copies: looking into rename from %s to %s\n' % (a, b))
132 132 am = a.manifest()
133 133 basemf = None if base is None else base.manifest()
134 134
135 135 # find where new files came from
136 136 # we currently don't try to find where old files went, too expensive
137 137 # this means we can miss a case like 'hg rm b; hg cp a b'
138 138 cm = {}
139 139
140 140 # Computing the forward missing is quite expensive on large manifests, since
141 141 # it compares the entire manifests. We can optimize it in the common use
142 142 # case of computing what copies are in a commit versus its parent (like
143 143 # during a rebase or histedit). Note, we exclude merge commits from this
144 144 # optimization, since the ctx.files() for a merge commit is not correct for
145 145 # this comparison.
146 146 forwardmissingmatch = match
147 147 if b.p1() == a and b.p2().node() == node.nullid:
148 148 filesmatcher = matchmod.exact(b.files())
149 149 forwardmissingmatch = matchmod.intersectmatchers(match, filesmatcher)
150 150 missing = _computeforwardmissing(a, b, match=forwardmissingmatch)
151 151
152 152 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
153 153
154 154 if debug:
155 155 dbg(b'debug.copies: missing files to search: %d\n' % len(missing))
156 156
157 157 for f in sorted(missing):
158 158 if debug:
159 159 dbg(b'debug.copies: tracing file: %s\n' % f)
160 160 fctx = b[f]
161 161 fctx._ancestrycontext = ancestrycontext
162 162
163 163 if debug:
164 164 start = util.timer()
165 165 opath = _tracefile(fctx, am, basemf)
166 166 if opath:
167 167 if debug:
168 168 dbg(b'debug.copies: rename of: %s\n' % opath)
169 169 cm[f] = opath
170 170 if debug:
171 171 dbg(
172 172 b'debug.copies: time: %f seconds\n'
173 173 % (util.timer() - start)
174 174 )
175 175 return cm
176 176
177 177
178 178 def _revinfogetter(repo):
179 179 """return a function that return multiple data given a <rev>"i
180 180
181 181 * p1: revision number of first parent
182 182 * p2: revision number of first parent
183 183 * p1copies: mapping of copies from p1
184 184 * p2copies: mapping of copies from p2
185 185 * removed: a list of removed files
186 * ismerged: a callback to know if file was merged in that revision
186 187 """
187 188 cl = repo.changelog
188 189 parents = cl.parentrevs
189 190
191 def get_ismerged(rev):
192 ctx = repo[rev]
193
194 def ismerged(path):
195 if path not in ctx.files():
196 return False
197 fctx = ctx[path]
198 parents = fctx._filelog.parents(fctx._filenode)
199 nb_parents = 0
200 for n in parents:
201 if n != node.nullid:
202 nb_parents += 1
203 return nb_parents >= 2
204
205 return ismerged
206
190 207 if repo.filecopiesmode == b'changeset-sidedata':
191 208 changelogrevision = cl.changelogrevision
192 209 flags = cl.flags
193 210
194 211 # A small cache to avoid doing the work twice for merges
195 212 #
196 213 # In the vast majority of cases, if we ask information for a revision
197 214 # about 1 parent, we'll later ask it for the other. So it make sense to
198 215 # keep the information around when reaching the first parent of a merge
199 216 # and dropping it after it was provided for the second parents.
200 217 #
201 218 # It exists cases were only one parent of the merge will be walked. It
202 219 # happens when the "destination" the copy tracing is descendant from a
203 220 # new root, not common with the "source". In that case, we will only walk
204 221 # through merge parents that are descendant of changesets common
205 222 # between "source" and "destination".
206 223 #
207 224 # With the current case implementation if such changesets have a copy
208 225 # information, we'll keep them in memory until the end of
209 226 # _changesetforwardcopies. We don't expect the case to be frequent
210 227 # enough to matters.
211 228 #
212 229 # In addition, it would be possible to reach pathological case, were
213 230 # many first parent are met before any second parent is reached. In
214 231 # that case the cache could grow. If this even become an issue one can
215 232 # safely introduce a maximum cache size. This would trade extra CPU/IO
216 233 # time to save memory.
217 234 merge_caches = {}
218 235
219 236 def revinfo(rev):
220 237 p1, p2 = parents(rev)
238 value = None
221 239 if flags(rev) & REVIDX_SIDEDATA:
222 240 e = merge_caches.pop(rev, None)
223 241 if e is not None:
224 242 return e
225 243 c = changelogrevision(rev)
226 244 p1copies = c.p1copies
227 245 p2copies = c.p2copies
228 246 removed = c.filesremoved
229 247 if p1 != node.nullrev and p2 != node.nullrev:
230 248 # XXX some case we over cache, IGNORE
231 merge_caches[rev] = (p1, p2, p1copies, p2copies, removed)
249 value = merge_caches[rev] = (
250 p1,
251 p2,
252 p1copies,
253 p2copies,
254 removed,
255 get_ismerged(rev),
256 )
232 257 else:
233 258 p1copies = {}
234 259 p2copies = {}
235 260 removed = []
236 return p1, p2, p1copies, p2copies, removed
261
262 if value is None:
263 value = (p1, p2, p1copies, p2copies, removed, get_ismerged(rev))
264 return value
237 265
238 266 else:
239 267
240 268 def revinfo(rev):
241 269 p1, p2 = parents(rev)
242 270 ctx = repo[rev]
243 271 p1copies, p2copies = ctx._copies
244 272 removed = ctx.filesremoved()
245 return p1, p2, p1copies, p2copies, removed
273 return p1, p2, p1copies, p2copies, removed, get_ismerged(rev)
246 274
247 275 return revinfo
248 276
249 277
250 278 def _changesetforwardcopies(a, b, match):
251 279 if a.rev() in (node.nullrev, b.rev()):
252 280 return {}
253 281
254 282 repo = a.repo().unfiltered()
255 283 children = {}
256 284 revinfo = _revinfogetter(repo)
257 285
258 286 cl = repo.changelog
287 isancestor = cl.isancestorrev # XXX we should had chaching to this.
259 288 missingrevs = cl.findmissingrevs(common=[a.rev()], heads=[b.rev()])
260 289 mrset = set(missingrevs)
261 290 roots = set()
262 291 for r in missingrevs:
263 292 for p in cl.parentrevs(r):
264 293 if p == node.nullrev:
265 294 continue
266 295 if p not in children:
267 296 children[p] = [r]
268 297 else:
269 298 children[p].append(r)
270 299 if p not in mrset:
271 300 roots.add(p)
272 301 if not roots:
273 302 # no common revision to track copies from
274 303 return {}
275 304 min_root = min(roots)
276 305
277 306 from_head = set(
278 307 cl.reachableroots(min_root, [b.rev()], list(roots), includepath=True)
279 308 )
280 309
281 310 iterrevs = set(from_head)
282 311 iterrevs &= mrset
283 312 iterrevs.update(roots)
284 313 iterrevs.remove(b.rev())
285 314 revs = sorted(iterrevs)
286 return _combinechangesetcopies(revs, children, b.rev(), revinfo, match)
315 return _combinechangesetcopies(
316 revs, children, b.rev(), revinfo, match, isancestor
317 )
287 318
288 319
289 def _combinechangesetcopies(revs, children, targetrev, revinfo, match):
320 def _combinechangesetcopies(
321 revs, children, targetrev, revinfo, match, isancestor
322 ):
290 323 """combine the copies information for each item of iterrevs
291 324
292 325 revs: sorted iterable of revision to visit
293 326 children: a {parent: [children]} mapping.
294 327 targetrev: the final copies destination revision (not in iterrevs)
295 328 revinfo(rev): a function that return (p1, p2, p1copies, p2copies, removed)
296 329 match: a matcher
297 330
298 331 It returns the aggregated copies information for `targetrev`.
299 332 """
300 333 all_copies = {}
301 334 alwaysmatch = match.always()
302 335 for r in revs:
303 336 copies = all_copies.pop(r, None)
304 337 if copies is None:
305 338 # this is a root
306 339 copies = {}
307 340 for i, c in enumerate(children[r]):
308 p1, p2, p1copies, p2copies, removed = revinfo(c)
341 p1, p2, p1copies, p2copies, removed, ismerged = revinfo(c)
309 342 if r == p1:
310 343 parent = 1
311 344 childcopies = p1copies
312 345 else:
313 346 assert r == p2
314 347 parent = 2
315 348 childcopies = p2copies
316 349 if not alwaysmatch:
317 350 childcopies = {
318 351 dst: src for dst, src in childcopies.items() if match(dst)
319 352 }
320 353 newcopies = copies
321 354 if childcopies:
322 newcopies = _chain(newcopies, childcopies)
323 # _chain makes a copies, we can avoid doing so in some
324 # simple/linear cases.
355 newcopies = copies.copy()
356 for dest, source in pycompat.iteritems(childcopies):
357 prev = copies.get(source)
358 if prev is not None and prev[1] is not None:
359 source = prev[1]
360 newcopies[dest] = (c, source)
325 361 assert newcopies is not copies
326 362 for f in removed:
327 363 if f in newcopies:
328 364 if newcopies is copies:
329 365 # copy on write to avoid affecting potential other
330 366 # branches. when there are no other branches, this
331 367 # could be avoided.
332 368 newcopies = copies.copy()
333 del newcopies[f]
369 newcopies[f] = (c, None)
334 370 othercopies = all_copies.get(c)
335 371 if othercopies is None:
336 372 all_copies[c] = newcopies
337 373 else:
338 374 # we are the second parent to work on c, we need to merge our
339 375 # work with the other.
340 376 #
341 # Unlike when copies are stored in the filelog, we consider
342 # it a copy even if the destination already existed on the
343 # other branch. It's simply too expensive to check if the
344 # file existed in the manifest.
345 #
346 377 # In case of conflict, parent 1 take precedence over parent 2.
347 378 # This is an arbitrary choice made anew when implementing
348 379 # changeset based copies. It was made without regards with
349 380 # potential filelog related behavior.
350 381 if parent == 1:
351 othercopies.update(newcopies)
382 _merge_copies_dict(
383 othercopies, newcopies, isancestor, ismerged
384 )
352 385 else:
353 newcopies.update(othercopies)
386 _merge_copies_dict(
387 newcopies, othercopies, isancestor, ismerged
388 )
354 389 all_copies[c] = newcopies
355 return all_copies[targetrev]
390
391 final_copies = {}
392 for dest, (tt, source) in all_copies[targetrev].items():
393 if source is not None:
394 final_copies[dest] = source
395 return final_copies
396
397
398 def _merge_copies_dict(minor, major, isancestor, ismerged):
399 """merge two copies-mapping together, minor and major
400
401 In case of conflict, value from "major" will be picked.
402
403 - `isancestors(low_rev, high_rev)`: callable return True if `low_rev` is an
404 ancestors of `high_rev`,
405
406 - `ismerged(path)`: callable return True if `path` have been merged in the
407 current revision,
408 """
409 for dest, value in major.items():
410 other = minor.get(dest)
411 if other is None:
412 minor[dest] = value
413 else:
414 new_tt = value[0]
415 other_tt = other[0]
416 if value[1] == other[1]:
417 continue
418 # content from "major" wins, unless it is older
419 # than the branch point or there is a merge
420 if (
421 new_tt == other_tt
422 or not isancestor(new_tt, other_tt)
423 or ismerged(dest)
424 ):
425 minor[dest] = value
356 426
357 427
358 428 def _forwardcopies(a, b, base=None, match=None):
359 429 """find {dst@b: src@a} copy mapping where a is an ancestor of b"""
360 430
361 431 if base is None:
362 432 base = a
363 433 match = a.repo().narrowmatch(match)
364 434 # check for working copy
365 435 if b.rev() is None:
366 436 cm = _committedforwardcopies(a, b.p1(), base, match)
367 437 # combine copies from dirstate if necessary
368 438 copies = _chain(cm, _dirstatecopies(b._repo, match))
369 439 else:
370 440 copies = _committedforwardcopies(a, b, base, match)
371 441 return copies
372 442
373 443
374 444 def _backwardrenames(a, b, match):
375 445 if a._repo.ui.config(b'experimental', b'copytrace') == b'off':
376 446 return {}
377 447
378 448 # Even though we're not taking copies into account, 1:n rename situations
379 449 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
380 450 # arbitrarily pick one of the renames.
381 451 # We don't want to pass in "match" here, since that would filter
382 452 # the destination by it. Since we're reversing the copies, we want
383 453 # to filter the source instead.
384 454 f = _forwardcopies(b, a)
385 455 r = {}
386 456 for k, v in sorted(pycompat.iteritems(f)):
387 457 if match and not match(v):
388 458 continue
389 459 # remove copies
390 460 if v in a:
391 461 continue
392 462 r[v] = k
393 463 return r
394 464
395 465
396 466 def pathcopies(x, y, match=None):
397 467 """find {dst@y: src@x} copy mapping for directed compare"""
398 468 repo = x._repo
399 469 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
400 470 if debug:
401 471 repo.ui.debug(
402 472 b'debug.copies: searching copies from %s to %s\n' % (x, y)
403 473 )
404 474 if x == y or not x or not y:
405 475 return {}
406 476 if y.rev() is None and x == y.p1():
407 477 if debug:
408 478 repo.ui.debug(b'debug.copies: search mode: dirstate\n')
409 479 # short-circuit to avoid issues with merge states
410 480 return _dirstatecopies(repo, match)
411 481 a = y.ancestor(x)
412 482 if a == x:
413 483 if debug:
414 484 repo.ui.debug(b'debug.copies: search mode: forward\n')
415 485 copies = _forwardcopies(x, y, match=match)
416 486 elif a == y:
417 487 if debug:
418 488 repo.ui.debug(b'debug.copies: search mode: backward\n')
419 489 copies = _backwardrenames(x, y, match=match)
420 490 else:
421 491 if debug:
422 492 repo.ui.debug(b'debug.copies: search mode: combined\n')
423 493 base = None
424 494 if a.rev() != node.nullrev:
425 495 base = x
426 496 copies = _chain(
427 497 _backwardrenames(x, a, match=match),
428 498 _forwardcopies(a, y, base, match=match),
429 499 )
430 500 _filter(x, y, copies)
431 501 return copies
432 502
433 503
434 504 def mergecopies(repo, c1, c2, base):
435 505 """
436 506 Finds moves and copies between context c1 and c2 that are relevant for
437 507 merging. 'base' will be used as the merge base.
438 508
439 509 Copytracing is used in commands like rebase, merge, unshelve, etc to merge
440 510 files that were moved/ copied in one merge parent and modified in another.
441 511 For example:
442 512
443 513 o ---> 4 another commit
444 514 |
445 515 | o ---> 3 commit that modifies a.txt
446 516 | /
447 517 o / ---> 2 commit that moves a.txt to b.txt
448 518 |/
449 519 o ---> 1 merge base
450 520
451 521 If we try to rebase revision 3 on revision 4, since there is no a.txt in
452 522 revision 4, and if user have copytrace disabled, we prints the following
453 523 message:
454 524
455 525 ```other changed <file> which local deleted```
456 526
457 527 Returns a tuple where:
458 528
459 529 "branch_copies" an instance of branch_copies.
460 530
461 531 "diverge" is a mapping of source name -> list of destination names
462 532 for divergent renames.
463 533
464 534 This function calls different copytracing algorithms based on config.
465 535 """
466 536 # avoid silly behavior for update from empty dir
467 537 if not c1 or not c2 or c1 == c2:
468 538 return branch_copies(), branch_copies(), {}
469 539
470 540 narrowmatch = c1.repo().narrowmatch()
471 541
472 542 # avoid silly behavior for parent -> working dir
473 543 if c2.node() is None and c1.node() == repo.dirstate.p1():
474 544 return (
475 545 branch_copies(_dirstatecopies(repo, narrowmatch)),
476 546 branch_copies(),
477 547 {},
478 548 )
479 549
480 550 copytracing = repo.ui.config(b'experimental', b'copytrace')
481 551 if stringutil.parsebool(copytracing) is False:
482 552 # stringutil.parsebool() returns None when it is unable to parse the
483 553 # value, so we should rely on making sure copytracing is on such cases
484 554 return branch_copies(), branch_copies(), {}
485 555
486 556 if usechangesetcentricalgo(repo):
487 557 # The heuristics don't make sense when we need changeset-centric algos
488 558 return _fullcopytracing(repo, c1, c2, base)
489 559
490 560 # Copy trace disabling is explicitly below the node == p1 logic above
491 561 # because the logic above is required for a simple copy to be kept across a
492 562 # rebase.
493 563 if copytracing == b'heuristics':
494 564 # Do full copytracing if only non-public revisions are involved as
495 565 # that will be fast enough and will also cover the copies which could
496 566 # be missed by heuristics
497 567 if _isfullcopytraceable(repo, c1, base):
498 568 return _fullcopytracing(repo, c1, c2, base)
499 569 return _heuristicscopytracing(repo, c1, c2, base)
500 570 else:
501 571 return _fullcopytracing(repo, c1, c2, base)
502 572
503 573
504 574 def _isfullcopytraceable(repo, c1, base):
505 575 """ Checks that if base, source and destination are all no-public branches,
506 576 if yes let's use the full copytrace algorithm for increased capabilities
507 577 since it will be fast enough.
508 578
509 579 `experimental.copytrace.sourcecommitlimit` can be used to set a limit for
510 580 number of changesets from c1 to base such that if number of changesets are
511 581 more than the limit, full copytracing algorithm won't be used.
512 582 """
513 583 if c1.rev() is None:
514 584 c1 = c1.p1()
515 585 if c1.mutable() and base.mutable():
516 586 sourcecommitlimit = repo.ui.configint(
517 587 b'experimental', b'copytrace.sourcecommitlimit'
518 588 )
519 589 commits = len(repo.revs(b'%d::%d', base.rev(), c1.rev()))
520 590 return commits < sourcecommitlimit
521 591 return False
522 592
523 593
524 594 def _checksinglesidecopies(
525 595 src, dsts1, m1, m2, mb, c2, base, copy, renamedelete
526 596 ):
527 597 if src not in m2:
528 598 # deleted on side 2
529 599 if src not in m1:
530 600 # renamed on side 1, deleted on side 2
531 601 renamedelete[src] = dsts1
532 602 elif src not in mb:
533 603 # Work around the "short-circuit to avoid issues with merge states"
534 604 # thing in pathcopies(): pathcopies(x, y) can return a copy where the
535 605 # destination doesn't exist in y.
536 606 pass
537 607 elif m2[src] != mb[src]:
538 608 if not _related(c2[src], base[src]):
539 609 return
540 610 # modified on side 2
541 611 for dst in dsts1:
542 612 copy[dst] = src
543 613
544 614
545 615 class branch_copies(object):
546 616 """Information about copies made on one side of a merge/graft.
547 617
548 618 "copy" is a mapping from destination name -> source name,
549 619 where source is in c1 and destination is in c2 or vice-versa.
550 620
551 621 "movewithdir" is a mapping from source name -> destination name,
552 622 where the file at source present in one context but not the other
553 623 needs to be moved to destination by the merge process, because the
554 624 other context moved the directory it is in.
555 625
556 626 "renamedelete" is a mapping of source name -> list of destination
557 627 names for files deleted in c1 that were renamed in c2 or vice-versa.
558 628
559 629 "dirmove" is a mapping of detected source dir -> destination dir renames.
560 630 This is needed for handling changes to new files previously grafted into
561 631 renamed directories.
562 632 """
563 633
564 634 def __init__(
565 635 self, copy=None, renamedelete=None, dirmove=None, movewithdir=None
566 636 ):
567 637 self.copy = {} if copy is None else copy
568 638 self.renamedelete = {} if renamedelete is None else renamedelete
569 639 self.dirmove = {} if dirmove is None else dirmove
570 640 self.movewithdir = {} if movewithdir is None else movewithdir
571 641
572 642
573 643 def _fullcopytracing(repo, c1, c2, base):
574 644 """ The full copytracing algorithm which finds all the new files that were
575 645 added from merge base up to the top commit and for each file it checks if
576 646 this file was copied from another file.
577 647
578 648 This is pretty slow when a lot of changesets are involved but will track all
579 649 the copies.
580 650 """
581 651 m1 = c1.manifest()
582 652 m2 = c2.manifest()
583 653 mb = base.manifest()
584 654
585 655 copies1 = pathcopies(base, c1)
586 656 copies2 = pathcopies(base, c2)
587 657
588 658 if not (copies1 or copies2):
589 659 return branch_copies(), branch_copies(), {}
590 660
591 661 inversecopies1 = {}
592 662 inversecopies2 = {}
593 663 for dst, src in copies1.items():
594 664 inversecopies1.setdefault(src, []).append(dst)
595 665 for dst, src in copies2.items():
596 666 inversecopies2.setdefault(src, []).append(dst)
597 667
598 668 copy1 = {}
599 669 copy2 = {}
600 670 diverge = {}
601 671 renamedelete1 = {}
602 672 renamedelete2 = {}
603 673 allsources = set(inversecopies1) | set(inversecopies2)
604 674 for src in allsources:
605 675 dsts1 = inversecopies1.get(src)
606 676 dsts2 = inversecopies2.get(src)
607 677 if dsts1 and dsts2:
608 678 # copied/renamed on both sides
609 679 if src not in m1 and src not in m2:
610 680 # renamed on both sides
611 681 dsts1 = set(dsts1)
612 682 dsts2 = set(dsts2)
613 683 # If there's some overlap in the rename destinations, we
614 684 # consider it not divergent. For example, if side 1 copies 'a'
615 685 # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c'
616 686 # and 'd' and deletes 'a'.
617 687 if dsts1 & dsts2:
618 688 for dst in dsts1 & dsts2:
619 689 copy1[dst] = src
620 690 copy2[dst] = src
621 691 else:
622 692 diverge[src] = sorted(dsts1 | dsts2)
623 693 elif src in m1 and src in m2:
624 694 # copied on both sides
625 695 dsts1 = set(dsts1)
626 696 dsts2 = set(dsts2)
627 697 for dst in dsts1 & dsts2:
628 698 copy1[dst] = src
629 699 copy2[dst] = src
630 700 # TODO: Handle cases where it was renamed on one side and copied
631 701 # on the other side
632 702 elif dsts1:
633 703 # copied/renamed only on side 1
634 704 _checksinglesidecopies(
635 705 src, dsts1, m1, m2, mb, c2, base, copy1, renamedelete1
636 706 )
637 707 elif dsts2:
638 708 # copied/renamed only on side 2
639 709 _checksinglesidecopies(
640 710 src, dsts2, m2, m1, mb, c1, base, copy2, renamedelete2
641 711 )
642 712
643 713 # find interesting file sets from manifests
644 714 addedinm1 = m1.filesnotin(mb, repo.narrowmatch())
645 715 addedinm2 = m2.filesnotin(mb, repo.narrowmatch())
646 716 u1 = sorted(addedinm1 - addedinm2)
647 717 u2 = sorted(addedinm2 - addedinm1)
648 718
649 719 header = b" unmatched files in %s"
650 720 if u1:
651 721 repo.ui.debug(b"%s:\n %s\n" % (header % b'local', b"\n ".join(u1)))
652 722 if u2:
653 723 repo.ui.debug(b"%s:\n %s\n" % (header % b'other', b"\n ".join(u2)))
654 724
655 725 if repo.ui.debugflag:
656 726 renamedeleteset = set()
657 727 divergeset = set()
658 728 for dsts in diverge.values():
659 729 divergeset.update(dsts)
660 730 for dsts in renamedelete1.values():
661 731 renamedeleteset.update(dsts)
662 732 for dsts in renamedelete2.values():
663 733 renamedeleteset.update(dsts)
664 734
665 735 repo.ui.debug(
666 736 b" all copies found (* = to merge, ! = divergent, "
667 737 b"% = renamed and deleted):\n"
668 738 )
669 739 for side, copies in ((b"local", copies1), (b"remote", copies2)):
670 740 if not copies:
671 741 continue
672 742 repo.ui.debug(b" on %s side:\n" % side)
673 743 for f in sorted(copies):
674 744 note = b""
675 745 if f in copy1 or f in copy2:
676 746 note += b"*"
677 747 if f in divergeset:
678 748 note += b"!"
679 749 if f in renamedeleteset:
680 750 note += b"%"
681 751 repo.ui.debug(
682 752 b" src: '%s' -> dst: '%s' %s\n" % (copies[f], f, note)
683 753 )
684 754 del renamedeleteset
685 755 del divergeset
686 756
687 757 repo.ui.debug(b" checking for directory renames\n")
688 758
689 759 dirmove1, movewithdir2 = _dir_renames(repo, c1, copy1, copies1, u2)
690 760 dirmove2, movewithdir1 = _dir_renames(repo, c2, copy2, copies2, u1)
691 761
692 762 branch_copies1 = branch_copies(copy1, renamedelete1, dirmove1, movewithdir1)
693 763 branch_copies2 = branch_copies(copy2, renamedelete2, dirmove2, movewithdir2)
694 764
695 765 return branch_copies1, branch_copies2, diverge
696 766
697 767
698 768 def _dir_renames(repo, ctx, copy, fullcopy, addedfiles):
699 769 """Finds moved directories and files that should move with them.
700 770
701 771 ctx: the context for one of the sides
702 772 copy: files copied on the same side (as ctx)
703 773 fullcopy: files copied on the same side (as ctx), including those that
704 774 merge.manifestmerge() won't care about
705 775 addedfiles: added files on the other side (compared to ctx)
706 776 """
707 777 # generate a directory move map
708 778 d = ctx.dirs()
709 779 invalid = set()
710 780 dirmove = {}
711 781
712 782 # examine each file copy for a potential directory move, which is
713 783 # when all the files in a directory are moved to a new directory
714 784 for dst, src in pycompat.iteritems(fullcopy):
715 785 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
716 786 if dsrc in invalid:
717 787 # already seen to be uninteresting
718 788 continue
719 789 elif dsrc in d and ddst in d:
720 790 # directory wasn't entirely moved locally
721 791 invalid.add(dsrc)
722 792 elif dsrc in dirmove and dirmove[dsrc] != ddst:
723 793 # files from the same directory moved to two different places
724 794 invalid.add(dsrc)
725 795 else:
726 796 # looks good so far
727 797 dirmove[dsrc] = ddst
728 798
729 799 for i in invalid:
730 800 if i in dirmove:
731 801 del dirmove[i]
732 802 del d, invalid
733 803
734 804 if not dirmove:
735 805 return {}, {}
736 806
737 807 dirmove = {k + b"/": v + b"/" for k, v in pycompat.iteritems(dirmove)}
738 808
739 809 for d in dirmove:
740 810 repo.ui.debug(
741 811 b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d])
742 812 )
743 813
744 814 movewithdir = {}
745 815 # check unaccounted nonoverlapping files against directory moves
746 816 for f in addedfiles:
747 817 if f not in fullcopy:
748 818 for d in dirmove:
749 819 if f.startswith(d):
750 820 # new file added in a directory that was moved, move it
751 821 df = dirmove[d] + f[len(d) :]
752 822 if df not in copy:
753 823 movewithdir[f] = df
754 824 repo.ui.debug(
755 825 b" pending file src: '%s' -> dst: '%s'\n"
756 826 % (f, df)
757 827 )
758 828 break
759 829
760 830 return dirmove, movewithdir
761 831
762 832
763 833 def _heuristicscopytracing(repo, c1, c2, base):
764 834 """ Fast copytracing using filename heuristics
765 835
766 836 Assumes that moves or renames are of following two types:
767 837
768 838 1) Inside a directory only (same directory name but different filenames)
769 839 2) Move from one directory to another
770 840 (same filenames but different directory names)
771 841
772 842 Works only when there are no merge commits in the "source branch".
773 843 Source branch is commits from base up to c2 not including base.
774 844
775 845 If merge is involved it fallbacks to _fullcopytracing().
776 846
777 847 Can be used by setting the following config:
778 848
779 849 [experimental]
780 850 copytrace = heuristics
781 851
782 852 In some cases the copy/move candidates found by heuristics can be very large
783 853 in number and that will make the algorithm slow. The number of possible
784 854 candidates to check can be limited by using the config
785 855 `experimental.copytrace.movecandidateslimit` which defaults to 100.
786 856 """
787 857
788 858 if c1.rev() is None:
789 859 c1 = c1.p1()
790 860 if c2.rev() is None:
791 861 c2 = c2.p1()
792 862
793 863 changedfiles = set()
794 864 m1 = c1.manifest()
795 865 if not repo.revs(b'%d::%d', base.rev(), c2.rev()):
796 866 # If base is not in c2 branch, we switch to fullcopytracing
797 867 repo.ui.debug(
798 868 b"switching to full copytracing as base is not "
799 869 b"an ancestor of c2\n"
800 870 )
801 871 return _fullcopytracing(repo, c1, c2, base)
802 872
803 873 ctx = c2
804 874 while ctx != base:
805 875 if len(ctx.parents()) == 2:
806 876 # To keep things simple let's not handle merges
807 877 repo.ui.debug(b"switching to full copytracing because of merges\n")
808 878 return _fullcopytracing(repo, c1, c2, base)
809 879 changedfiles.update(ctx.files())
810 880 ctx = ctx.p1()
811 881
812 882 copies2 = {}
813 883 cp = _forwardcopies(base, c2)
814 884 for dst, src in pycompat.iteritems(cp):
815 885 if src in m1:
816 886 copies2[dst] = src
817 887
818 888 # file is missing if it isn't present in the destination, but is present in
819 889 # the base and present in the source.
820 890 # Presence in the base is important to exclude added files, presence in the
821 891 # source is important to exclude removed files.
822 892 filt = lambda f: f not in m1 and f in base and f in c2
823 893 missingfiles = [f for f in changedfiles if filt(f)]
824 894
825 895 copies1 = {}
826 896 if missingfiles:
827 897 basenametofilename = collections.defaultdict(list)
828 898 dirnametofilename = collections.defaultdict(list)
829 899
830 900 for f in m1.filesnotin(base.manifest()):
831 901 basename = os.path.basename(f)
832 902 dirname = os.path.dirname(f)
833 903 basenametofilename[basename].append(f)
834 904 dirnametofilename[dirname].append(f)
835 905
836 906 for f in missingfiles:
837 907 basename = os.path.basename(f)
838 908 dirname = os.path.dirname(f)
839 909 samebasename = basenametofilename[basename]
840 910 samedirname = dirnametofilename[dirname]
841 911 movecandidates = samebasename + samedirname
842 912 # f is guaranteed to be present in c2, that's why
843 913 # c2.filectx(f) won't fail
844 914 f2 = c2.filectx(f)
845 915 # we can have a lot of candidates which can slow down the heuristics
846 916 # config value to limit the number of candidates moves to check
847 917 maxcandidates = repo.ui.configint(
848 918 b'experimental', b'copytrace.movecandidateslimit'
849 919 )
850 920
851 921 if len(movecandidates) > maxcandidates:
852 922 repo.ui.status(
853 923 _(
854 924 b"skipping copytracing for '%s', more "
855 925 b"candidates than the limit: %d\n"
856 926 )
857 927 % (f, len(movecandidates))
858 928 )
859 929 continue
860 930
861 931 for candidate in movecandidates:
862 932 f1 = c1.filectx(candidate)
863 933 if _related(f1, f2):
864 934 # if there are a few related copies then we'll merge
865 935 # changes into all of them. This matches the behaviour
866 936 # of upstream copytracing
867 937 copies1[candidate] = f
868 938
869 939 return branch_copies(copies1), branch_copies(copies2), {}
870 940
871 941
872 942 def _related(f1, f2):
873 943 """return True if f1 and f2 filectx have a common ancestor
874 944
875 945 Walk back to common ancestor to see if the two files originate
876 946 from the same file. Since workingfilectx's rev() is None it messes
877 947 up the integer comparison logic, hence the pre-step check for
878 948 None (f1 and f2 can only be workingfilectx's initially).
879 949 """
880 950
881 951 if f1 == f2:
882 952 return True # a match
883 953
884 954 g1, g2 = f1.ancestors(), f2.ancestors()
885 955 try:
886 956 f1r, f2r = f1.linkrev(), f2.linkrev()
887 957
888 958 if f1r is None:
889 959 f1 = next(g1)
890 960 if f2r is None:
891 961 f2 = next(g2)
892 962
893 963 while True:
894 964 f1r, f2r = f1.linkrev(), f2.linkrev()
895 965 if f1r > f2r:
896 966 f1 = next(g1)
897 967 elif f2r > f1r:
898 968 f2 = next(g2)
899 969 else: # f1 and f2 point to files in the same linkrev
900 970 return f1 == f2 # true if they point to the same file
901 971 except StopIteration:
902 972 return False
903 973
904 974
905 975 def graftcopies(wctx, ctx, base):
906 976 """reproduce copies between base and ctx in the wctx
907 977
908 978 Unlike mergecopies(), this function will only consider copies between base
909 979 and ctx; it will ignore copies between base and wctx. Also unlike
910 980 mergecopies(), this function will apply copies to the working copy (instead
911 981 of just returning information about the copies). That makes it cheaper
912 982 (especially in the common case of base==ctx.p1()) and useful also when
913 983 experimental.copytrace=off.
914 984
915 985 merge.update() will have already marked most copies, but it will only
916 986 mark copies if it thinks the source files are related (see
917 987 merge._related()). It will also not mark copies if the file wasn't modified
918 988 on the local side. This function adds the copies that were "missed"
919 989 by merge.update().
920 990 """
921 991 new_copies = pathcopies(base, ctx)
922 992 _filter(wctx.p1(), wctx, new_copies)
923 993 for dst, src in pycompat.iteritems(new_copies):
924 994 wctx[dst].markcopied(src)
925 995
926 996
927 997 def computechangesetfilesadded(ctx):
928 998 """return the list of files added in a changeset
929 999 """
930 1000 added = []
931 1001 for f in ctx.files():
932 1002 if not any(f in p for p in ctx.parents()):
933 1003 added.append(f)
934 1004 return added
935 1005
936 1006
937 1007 def computechangesetfilesremoved(ctx):
938 1008 """return the list of files removed in a changeset
939 1009 """
940 1010 removed = []
941 1011 for f in ctx.files():
942 1012 if f not in ctx:
943 1013 removed.append(f)
944 1014 return removed
945 1015
946 1016
947 1017 def computechangesetcopies(ctx):
948 1018 """return the copies data for a changeset
949 1019
950 1020 The copies data are returned as a pair of dictionnary (p1copies, p2copies).
951 1021
952 1022 Each dictionnary are in the form: `{newname: oldname}`
953 1023 """
954 1024 p1copies = {}
955 1025 p2copies = {}
956 1026 p1 = ctx.p1()
957 1027 p2 = ctx.p2()
958 1028 narrowmatch = ctx._repo.narrowmatch()
959 1029 for dst in ctx.files():
960 1030 if not narrowmatch(dst) or dst not in ctx:
961 1031 continue
962 1032 copied = ctx[dst].renamed()
963 1033 if not copied:
964 1034 continue
965 1035 src, srcnode = copied
966 1036 if src in p1 and p1[src].filenode() == srcnode:
967 1037 p1copies[dst] = src
968 1038 elif src in p2 and p2[src].filenode() == srcnode:
969 1039 p2copies[dst] = src
970 1040 return p1copies, p2copies
971 1041
972 1042
973 1043 def encodecopies(files, copies):
974 1044 items = []
975 1045 for i, dst in enumerate(files):
976 1046 if dst in copies:
977 1047 items.append(b'%d\0%s' % (i, copies[dst]))
978 1048 if len(items) != len(copies):
979 1049 raise error.ProgrammingError(
980 1050 b'some copy targets missing from file list'
981 1051 )
982 1052 return b"\n".join(items)
983 1053
984 1054
985 1055 def decodecopies(files, data):
986 1056 try:
987 1057 copies = {}
988 1058 if not data:
989 1059 return copies
990 1060 for l in data.split(b'\n'):
991 1061 strindex, src = l.split(b'\0')
992 1062 i = int(strindex)
993 1063 dst = files[i]
994 1064 copies[dst] = src
995 1065 return copies
996 1066 except (ValueError, IndexError):
997 1067 # Perhaps someone had chosen the same key name (e.g. "p1copies") and
998 1068 # used different syntax for the value.
999 1069 return None
1000 1070
1001 1071
1002 1072 def encodefileindices(files, subset):
1003 1073 subset = set(subset)
1004 1074 indices = []
1005 1075 for i, f in enumerate(files):
1006 1076 if f in subset:
1007 1077 indices.append(b'%d' % i)
1008 1078 return b'\n'.join(indices)
1009 1079
1010 1080
1011 1081 def decodefileindices(files, data):
1012 1082 try:
1013 1083 subset = []
1014 1084 if not data:
1015 1085 return subset
1016 1086 for strindex in data.split(b'\n'):
1017 1087 i = int(strindex)
1018 1088 if i < 0 or i >= len(files):
1019 1089 return None
1020 1090 subset.append(files[i])
1021 1091 return subset
1022 1092 except (ValueError, IndexError):
1023 1093 # Perhaps someone had chosen the same key name (e.g. "added") and
1024 1094 # used different syntax for the value.
1025 1095 return None
1026 1096
1027 1097
1028 1098 def _getsidedata(srcrepo, rev):
1029 1099 ctx = srcrepo[rev]
1030 1100 filescopies = computechangesetcopies(ctx)
1031 1101 filesadded = computechangesetfilesadded(ctx)
1032 1102 filesremoved = computechangesetfilesremoved(ctx)
1033 1103 sidedata = {}
1034 1104 if any([filescopies, filesadded, filesremoved]):
1035 1105 sortedfiles = sorted(ctx.files())
1036 1106 p1copies, p2copies = filescopies
1037 1107 p1copies = encodecopies(sortedfiles, p1copies)
1038 1108 p2copies = encodecopies(sortedfiles, p2copies)
1039 1109 filesadded = encodefileindices(sortedfiles, filesadded)
1040 1110 filesremoved = encodefileindices(sortedfiles, filesremoved)
1041 1111 if p1copies:
1042 1112 sidedata[sidedatamod.SD_P1COPIES] = p1copies
1043 1113 if p2copies:
1044 1114 sidedata[sidedatamod.SD_P2COPIES] = p2copies
1045 1115 if filesadded:
1046 1116 sidedata[sidedatamod.SD_FILESADDED] = filesadded
1047 1117 if filesremoved:
1048 1118 sidedata[sidedatamod.SD_FILESREMOVED] = filesremoved
1049 1119 return sidedata
1050 1120
1051 1121
1052 1122 def getsidedataadder(srcrepo, destrepo):
1053 1123 use_w = srcrepo.ui.configbool(b'experimental', b'worker.repository-upgrade')
1054 1124 if pycompat.iswindows or not use_w:
1055 1125 return _get_simple_sidedata_adder(srcrepo, destrepo)
1056 1126 else:
1057 1127 return _get_worker_sidedata_adder(srcrepo, destrepo)
1058 1128
1059 1129
1060 1130 def _sidedata_worker(srcrepo, revs_queue, sidedata_queue, tokens):
1061 1131 """The function used by worker precomputing sidedata
1062 1132
1063 1133 It read an input queue containing revision numbers
1064 1134 It write in an output queue containing (rev, <sidedata-map>)
1065 1135
1066 1136 The `None` input value is used as a stop signal.
1067 1137
1068 1138 The `tokens` semaphore is user to avoid having too many unprocessed
1069 1139 entries. The workers needs to acquire one token before fetching a task.
1070 1140 They will be released by the consumer of the produced data.
1071 1141 """
1072 1142 tokens.acquire()
1073 1143 rev = revs_queue.get()
1074 1144 while rev is not None:
1075 1145 data = _getsidedata(srcrepo, rev)
1076 1146 sidedata_queue.put((rev, data))
1077 1147 tokens.acquire()
1078 1148 rev = revs_queue.get()
1079 1149 # processing of `None` is completed, release the token.
1080 1150 tokens.release()
1081 1151
1082 1152
1083 1153 BUFF_PER_WORKER = 50
1084 1154
1085 1155
1086 1156 def _get_worker_sidedata_adder(srcrepo, destrepo):
1087 1157 """The parallel version of the sidedata computation
1088 1158
1089 1159 This code spawn a pool of worker that precompute a buffer of sidedata
1090 1160 before we actually need them"""
1091 1161 # avoid circular import copies -> scmutil -> worker -> copies
1092 1162 from . import worker
1093 1163
1094 1164 nbworkers = worker._numworkers(srcrepo.ui)
1095 1165
1096 1166 tokens = multiprocessing.BoundedSemaphore(nbworkers * BUFF_PER_WORKER)
1097 1167 revsq = multiprocessing.Queue()
1098 1168 sidedataq = multiprocessing.Queue()
1099 1169
1100 1170 assert srcrepo.filtername is None
1101 1171 # queue all tasks beforehand, revision numbers are small and it make
1102 1172 # synchronisation simpler
1103 1173 #
1104 1174 # Since the computation for each node can be quite expensive, the overhead
1105 1175 # of using a single queue is not revelant. In practice, most computation
1106 1176 # are fast but some are very expensive and dominate all the other smaller
1107 1177 # cost.
1108 1178 for r in srcrepo.changelog.revs():
1109 1179 revsq.put(r)
1110 1180 # queue the "no more tasks" markers
1111 1181 for i in range(nbworkers):
1112 1182 revsq.put(None)
1113 1183
1114 1184 allworkers = []
1115 1185 for i in range(nbworkers):
1116 1186 args = (srcrepo, revsq, sidedataq, tokens)
1117 1187 w = multiprocessing.Process(target=_sidedata_worker, args=args)
1118 1188 allworkers.append(w)
1119 1189 w.start()
1120 1190
1121 1191 # dictionnary to store results for revision higher than we one we are
1122 1192 # looking for. For example, if we need the sidedatamap for 42, and 43 is
1123 1193 # received, when shelve 43 for later use.
1124 1194 staging = {}
1125 1195
1126 1196 def sidedata_companion(revlog, rev):
1127 1197 sidedata = {}
1128 1198 if util.safehasattr(revlog, b'filteredrevs'): # this is a changelog
1129 1199 # Is the data previously shelved ?
1130 1200 sidedata = staging.pop(rev, None)
1131 1201 if sidedata is None:
1132 1202 # look at the queued result until we find the one we are lookig
1133 1203 # for (shelve the other ones)
1134 1204 r, sidedata = sidedataq.get()
1135 1205 while r != rev:
1136 1206 staging[r] = sidedata
1137 1207 r, sidedata = sidedataq.get()
1138 1208 tokens.release()
1139 1209 return False, (), sidedata
1140 1210
1141 1211 return sidedata_companion
1142 1212
1143 1213
1144 1214 def _get_simple_sidedata_adder(srcrepo, destrepo):
1145 1215 """The simple version of the sidedata computation
1146 1216
1147 1217 It just compute it in the same thread on request"""
1148 1218
1149 1219 def sidedatacompanion(revlog, rev):
1150 1220 sidedata = {}
1151 1221 if util.safehasattr(revlog, 'filteredrevs'): # this is a changelog
1152 1222 sidedata = _getsidedata(srcrepo, rev)
1153 1223 return False, (), sidedata
1154 1224
1155 1225 return sidedatacompanion
1156 1226
1157 1227
1158 1228 def getsidedataremover(srcrepo, destrepo):
1159 1229 def sidedatacompanion(revlog, rev):
1160 1230 f = ()
1161 1231 if util.safehasattr(revlog, 'filteredrevs'): # this is a changelog
1162 1232 if revlog.flags(rev) & REVIDX_SIDEDATA:
1163 1233 f = (
1164 1234 sidedatamod.SD_P1COPIES,
1165 1235 sidedatamod.SD_P2COPIES,
1166 1236 sidedatamod.SD_FILESADDED,
1167 1237 sidedatamod.SD_FILESREMOVED,
1168 1238 )
1169 1239 return False, f, {}
1170 1240
1171 1241 return sidedatacompanion
@@ -1,798 +1,855 b''
1 #testcases filelog compatibility sidedata
2
1 3 =====================================================
2 4 Test Copy tracing for chain of copies involving merge
3 5 =====================================================
4 6
5 7 This test files covers copies/rename case for a chains of commit where merges
6 8 are involved. It cheks we do not have unwanted update of behavior and that the
7 9 different options to retrieve copies behave correctly.
8 10
11
9 12 Setup
10 13 =====
11 14
12 15 use git diff to see rename
13 16
14 17 $ cat << EOF >> $HGRCPATH
15 18 > [diff]
16 19 > git=yes
17 20 > [ui]
18 21 > logtemplate={rev} {desc}\n
19 22 > EOF
20 23
24 #if compatibility
25 $ cat >> $HGRCPATH << EOF
26 > [experimental]
27 > copies.read-from = compatibility
28 > EOF
29 #endif
30
31 #if sidedata
32 $ cat >> $HGRCPATH << EOF
33 > [format]
34 > exp-use-side-data = yes
35 > exp-use-copies-side-data-changeset = yes
36 > EOF
37 #endif
38
39
21 40 $ hg init repo-chain
22 41 $ cd repo-chain
23 42
24 43 Add some linear rename initialy
25 44
26 45 $ touch a b h
27 46 $ hg ci -Am 'i-0 initial commit: a b h'
28 47 adding a
29 48 adding b
30 49 adding h
31 50 $ hg mv a c
32 51 $ hg ci -Am 'i-1: a -move-> c'
33 52 $ hg mv c d
34 53 $ hg ci -Am 'i-2: c -move-> d'
35 54 $ hg log -G
36 55 @ 2 i-2: c -move-> d
37 56 |
38 57 o 1 i-1: a -move-> c
39 58 |
40 59 o 0 i-0 initial commit: a b h
41 60
42 61
43 62 And having another branch with renames on the other side
44 63
45 64 $ hg mv d e
46 65 $ hg ci -Am 'a-1: d -move-> e'
47 66 $ hg mv e f
48 67 $ hg ci -Am 'a-2: e -move-> f'
49 68 $ hg log -G --rev '::.'
50 69 @ 4 a-2: e -move-> f
51 70 |
52 71 o 3 a-1: d -move-> e
53 72 |
54 73 o 2 i-2: c -move-> d
55 74 |
56 75 o 1 i-1: a -move-> c
57 76 |
58 77 o 0 i-0 initial commit: a b h
59 78
60 79
61 80 Have a branching with nothing on one side
62 81
63 82 $ hg up 'desc("i-2")'
64 83 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
65 84 $ echo foo > b
66 85 $ hg ci -m 'b-1: b update'
67 86 created new head
68 87 $ hg log -G --rev '::.'
69 88 @ 5 b-1: b update
70 89 |
71 90 o 2 i-2: c -move-> d
72 91 |
73 92 o 1 i-1: a -move-> c
74 93 |
75 94 o 0 i-0 initial commit: a b h
76 95
77 96
78 97 Create a branch that delete a file previous renamed
79 98
80 99 $ hg up 'desc("i-2")'
81 100 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
82 101 $ hg rm d
83 102 $ hg ci -m 'c-1 delete d'
84 103 created new head
85 104 $ hg log -G --rev '::.'
86 105 @ 6 c-1 delete d
87 106 |
88 107 o 2 i-2: c -move-> d
89 108 |
90 109 o 1 i-1: a -move-> c
91 110 |
92 111 o 0 i-0 initial commit: a b h
93 112
94 113
95 114 Create a branch that delete a file previous renamed and recreate it
96 115
97 116 $ hg up 'desc("i-2")'
98 117 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
99 118 $ hg rm d
100 119 $ hg ci -m 'd-1 delete d'
101 120 created new head
102 121 $ echo bar > d
103 122 $ hg add d
104 123 $ hg ci -m 'd-2 re-add d'
105 124 $ hg log -G --rev '::.'
106 125 @ 8 d-2 re-add d
107 126 |
108 127 o 7 d-1 delete d
109 128 |
110 129 o 2 i-2: c -move-> d
111 130 |
112 131 o 1 i-1: a -move-> c
113 132 |
114 133 o 0 i-0 initial commit: a b h
115 134
116 135
117 136 Having another branch renaming a different file to the same filename as another
118 137
119 138 $ hg up 'desc("i-2")'
120 139 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
121 140 $ hg mv b g
122 141 $ hg ci -m 'e-1 b -move-> g'
123 142 created new head
124 143 $ hg mv g f
125 144 $ hg ci -m 'e-2 g -move-> f'
126 145 $ hg log -G --rev '::.'
127 146 @ 10 e-2 g -move-> f
128 147 |
129 148 o 9 e-1 b -move-> g
130 149 |
131 150 o 2 i-2: c -move-> d
132 151 |
133 152 o 1 i-1: a -move-> c
134 153 |
135 154 o 0 i-0 initial commit: a b h
136 155
137 156
138 157 merging with unrelated change does not interfere with the renames
139 158 ---------------------------------------------------------------
140 159
141 160 - rename on one side
142 161 - unrelated change on the other side
143 162
144 163 $ hg up 'desc("b-1")'
145 164 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
146 165 $ hg merge 'desc("a-2")'
147 166 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
148 167 (branch merge, don't forget to commit)
149 168 $ hg ci -m 'mBAm-0 simple merge - one way'
150 169 $ hg up 'desc("a-2")'
151 170 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
152 171 $ hg merge 'desc("b-1")'
153 172 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
154 173 (branch merge, don't forget to commit)
155 174 $ hg ci -m 'mABm-0 simple merge - the other way'
156 175 created new head
157 176 $ hg log -G --rev '::(desc("mABm")+desc("mBAm"))'
158 177 @ 12 mABm-0 simple merge - the other way
159 178 |\
160 179 +---o 11 mBAm-0 simple merge - one way
161 180 | |/
162 181 | o 5 b-1: b update
163 182 | |
164 183 o | 4 a-2: e -move-> f
165 184 | |
166 185 o | 3 a-1: d -move-> e
167 186 |/
168 187 o 2 i-2: c -move-> d
169 188 |
170 189 o 1 i-1: a -move-> c
171 190 |
172 191 o 0 i-0 initial commit: a b h
173 192
174 193
175 194 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mABm")'
176 195 A f
177 196 d
178 197 R d
179 198 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mBAm")'
180 199 A f
181 200 d
182 201 R d
183 202 $ hg status --copies --rev 'desc("a-2")' --rev 'desc("mABm")'
184 203 M b
185 204 $ hg status --copies --rev 'desc("a-2")' --rev 'desc("mBAm")'
186 205 M b
187 206 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mABm")'
188 207 M b
189 208 A f
190 209 d
191 210 R d
192 211 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mBAm")'
193 212 M b
194 213 A f
195 214 d
196 215 R d
197 216 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mABm")'
198 217 M b
199 218 A f
200 219 a
201 220 R a
202 221 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mBAm")'
203 222 M b
204 223 A f
205 224 a
206 225 R a
207 226
208 227 merging with the side having a delete
209 228 -------------------------------------
210 229
211 230 case summary:
212 231 - one with change to an unrelated file
213 232 - one deleting the change
214 233 and recreate an unrelated file after the merge
215 234
216 235 $ hg up 'desc("b-1")'
217 236 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
218 237 $ hg merge 'desc("c-1")'
219 238 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
220 239 (branch merge, don't forget to commit)
221 240 $ hg ci -m 'mBCm-0 simple merge - one way'
222 241 $ echo bar > d
223 242 $ hg add d
224 243 $ hg ci -m 'mBCm-1 re-add d'
225 244 $ hg up 'desc("c-1")'
226 245 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
227 246 $ hg merge 'desc("b-1")'
228 247 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
229 248 (branch merge, don't forget to commit)
230 249 $ hg ci -m 'mCBm-0 simple merge - the other way'
231 250 created new head
232 251 $ echo bar > d
233 252 $ hg add d
234 253 $ hg ci -m 'mCBm-1 re-add d'
235 254 $ hg log -G --rev '::(desc("mCBm")+desc("mBCm"))'
236 255 @ 16 mCBm-1 re-add d
237 256 |
238 257 o 15 mCBm-0 simple merge - the other way
239 258 |\
240 259 | | o 14 mBCm-1 re-add d
241 260 | | |
242 261 +---o 13 mBCm-0 simple merge - one way
243 262 | |/
244 263 | o 6 c-1 delete d
245 264 | |
246 265 o | 5 b-1: b update
247 266 |/
248 267 o 2 i-2: c -move-> d
249 268 |
250 269 o 1 i-1: a -move-> c
251 270 |
252 271 o 0 i-0 initial commit: a b h
253 272
254 273 - comparing from the merge
255 274
256 275 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mBCm-0")'
257 276 R d
258 277 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mCBm-0")'
259 278 R d
260 279 $ hg status --copies --rev 'desc("c-1")' --rev 'desc("mBCm-0")'
261 280 M b
262 281 $ hg status --copies --rev 'desc("c-1")' --rev 'desc("mCBm-0")'
263 282 M b
264 283 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mBCm-0")'
265 284 M b
266 285 R d
267 286 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mCBm-0")'
268 287 M b
269 288 R d
270 289 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mBCm-0")'
271 290 M b
272 291 R a
273 292 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mCBm-0")'
274 293 M b
275 294 R a
276 295
277 296 - comparing with the merge children re-adding the file
278 297
279 298 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mBCm-1")'
280 299 M d
281 300 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mCBm-1")'
282 301 M d
283 302 $ hg status --copies --rev 'desc("c-1")' --rev 'desc("mBCm-1")'
284 303 M b
285 304 A d
286 305 $ hg status --copies --rev 'desc("c-1")' --rev 'desc("mCBm-1")'
287 306 M b
288 307 A d
289 308 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mBCm-1")'
290 309 M b
291 310 M d
292 311 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mCBm-1")'
293 312 M b
294 313 M d
295 314 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mBCm-1")'
296 315 M b
297 316 A d
298 317 R a
299 318 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mCBm-1")'
300 319 M b
301 320 A d
302 321 R a
303 322
304 323 Comparing with a merge re-adding the file afterward
305 324 ---------------------------------------------------
306 325
307 326 Merge:
308 327 - one with change to an unrelated file
309 328 - one deleting and recreating the change
310 329
311 330 Note:
312 331 | In this case, one of the merge wrongly record a merge while there is none.
313 332 | This lead to bad copy tracing information to be dug up.
314 333
315 334 $ hg up 'desc("b-1")'
316 335 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
317 336 $ hg merge 'desc("d-2")'
318 337 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
319 338 (branch merge, don't forget to commit)
320 339 $ hg ci -m 'mBDm-0 simple merge - one way'
321 340 $ hg up 'desc("d-2")'
322 341 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
323 342 $ hg merge 'desc("b-1")'
324 343 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
325 344 (branch merge, don't forget to commit)
326 345 $ hg ci -m 'mDBm-0 simple merge - the other way'
327 346 created new head
328 347 $ hg log -G --rev '::(desc("mDBm")+desc("mBDm"))'
329 348 @ 18 mDBm-0 simple merge - the other way
330 349 |\
331 350 +---o 17 mBDm-0 simple merge - one way
332 351 | |/
333 352 | o 8 d-2 re-add d
334 353 | |
335 354 | o 7 d-1 delete d
336 355 | |
337 356 o | 5 b-1: b update
338 357 |/
339 358 o 2 i-2: c -move-> d
340 359 |
341 360 o 1 i-1: a -move-> c
342 361 |
343 362 o 0 i-0 initial commit: a b h
344 363
345 364 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mBDm-0")'
346 365 M d
347 366 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mDBm-0")'
348 367 M d
349 368 $ hg status --copies --rev 'desc("d-2")' --rev 'desc("mBDm-0")'
350 369 M b
351 370 $ hg status --copies --rev 'desc("d-2")' --rev 'desc("mDBm-0")'
352 371 M b
353 372 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mBDm-0")'
354 373 M b
355 374 M d
356 375 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mDBm-0")'
357 376 M b
358 377 M d
359 378
360 379 The bugs makes recorded copy is different depending of where we started the merge from since
361 380
362 381 $ hg manifest --debug --rev 'desc("mBDm-0")' | grep '644 d'
363 382 b004912a8510032a0350a74daa2803dadfb00e12 644 d
364 383 $ hg manifest --debug --rev 'desc("mDBm-0")' | grep '644 d'
365 384 b004912a8510032a0350a74daa2803dadfb00e12 644 d
366 385
367 386 The 0bb5445dc4d02f4e0d86cf16f9f3a411d0f17744 entry is wrong, since the file was
368 387 deleted on one side (then recreate) and untouched on the other side, no "merge"
369 388 has happened. The resulting `d` file is the untouched version from branch `D`,
370 389 not a merge.
371 390
372 391 $ hg manifest --debug --rev 'desc("d-2")' | grep '644 d'
373 392 b004912a8510032a0350a74daa2803dadfb00e12 644 d
374 393 $ hg manifest --debug --rev 'desc("b-1")' | grep '644 d'
375 394 01c2f5eabdc4ce2bdee42b5f86311955e6c8f573 644 d
376 395 $ hg debugindex d
377 396 rev linkrev nodeid p1 p2
378 397 0 2 01c2f5eabdc4 000000000000 000000000000
379 398 1 8 b004912a8510 000000000000 000000000000
380 399
381 400 (This `hg log` output if wrong, since no merge actually happened).
382 401
383 402 $ hg log -Gfr 'desc("mBDm-0")' d
384 403 o 8 d-2 re-add d
385 404 |
386 405 ~
387 406
388 407 This `hg log` output is correct
389 408
390 409 $ hg log -Gfr 'desc("mDBm-0")' d
391 410 o 8 d-2 re-add d
392 411 |
393 412 ~
394 413
395 414 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mBDm-0")'
396 415 M b
397 416 A d
398 417 R a
399 418 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mDBm-0")'
400 419 M b
401 420 A d
402 421 R a
403 422
404 423
405 424 Comparing with a merge with colliding rename
406 425 --------------------------------------------
407 426
408 427 - the "e-" branch renaming b to f (through 'g')
409 428 - the "a-" branch renaming d to f (through e)
410 429
411 430 $ hg up 'desc("a-2")'
412 431 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
413 432 $ hg merge 'desc("e-2")'
414 433 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
415 434 (branch merge, don't forget to commit)
416 435 $ hg ci -m 'mAEm-0 simple merge - one way'
417 436 $ hg up 'desc("e-2")'
418 437 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
419 438 $ hg merge 'desc("a-2")'
420 439 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
421 440 (branch merge, don't forget to commit)
422 441 $ hg ci -m 'mEAm-0 simple merge - the other way'
423 442 created new head
424 443 $ hg log -G --rev '::(desc("mAEm")+desc("mEAm"))'
425 444 @ 20 mEAm-0 simple merge - the other way
426 445 |\
427 446 +---o 19 mAEm-0 simple merge - one way
428 447 | |/
429 448 | o 10 e-2 g -move-> f
430 449 | |
431 450 | o 9 e-1 b -move-> g
432 451 | |
433 452 o | 4 a-2: e -move-> f
434 453 | |
435 454 o | 3 a-1: d -move-> e
436 455 |/
437 456 o 2 i-2: c -move-> d
438 457 |
439 458 o 1 i-1: a -move-> c
440 459 |
441 460 o 0 i-0 initial commit: a b h
442 461
443 462 $ hg manifest --debug --rev 'desc("mAEm-0")' | grep '644 f'
444 463 eb806e34ef6be4c264effd5933d31004ad15a793 644 f
445 464 $ hg manifest --debug --rev 'desc("mEAm-0")' | grep '644 f'
446 465 eb806e34ef6be4c264effd5933d31004ad15a793 644 f
447 466 $ hg manifest --debug --rev 'desc("a-2")' | grep '644 f'
448 467 0dd616bc7ab1a111921d95d76f69cda5c2ac539c 644 f
449 468 $ hg manifest --debug --rev 'desc("e-2")' | grep '644 f'
450 469 6da5a2eecb9c833f830b67a4972366d49a9a142c 644 f
451 470 $ hg debugindex f
452 471 rev linkrev nodeid p1 p2
453 472 0 4 0dd616bc7ab1 000000000000 000000000000
454 473 1 10 6da5a2eecb9c 000000000000 000000000000
455 474 2 19 eb806e34ef6b 0dd616bc7ab1 6da5a2eecb9c
475
476 # Here the filelog based implementation is not looking at the rename
477 # information (because the file exist on both side). However the changelog
478 # based on works fine. We have different output.
479
456 480 $ hg status --copies --rev 'desc("a-2")' --rev 'desc("mAEm-0")'
457 481 M f
482 b (no-filelog !)
458 483 R b
459 484 $ hg status --copies --rev 'desc("a-2")' --rev 'desc("mEAm-0")'
460 485 M f
486 b (no-filelog !)
461 487 R b
462 488 $ hg status --copies --rev 'desc("e-2")' --rev 'desc("mAEm-0")'
463 489 M f
490 d (no-filelog !)
464 491 R d
465 492 $ hg status --copies --rev 'desc("e-2")' --rev 'desc("mEAm-0")'
466 493 M f
494 d (no-filelog !)
467 495 R d
468 496 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("a-2")'
469 497 A f
470 498 d
471 499 R d
472 500 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("e-2")'
473 501 A f
474 502 b
475 503 R b
504
505 # From here, we run status against revision where both source file exists.
506 #
507 # The filelog based implementation picks an arbitrary side based on revision
508 # numbers. So the same side "wins" whatever the parents order is. This is
509 # sub-optimal because depending on revision numbers means the result can be
510 # different from one repository to the next.
511 #
512 # The changeset based algorithm use the parent order to break tie on conflicting
513 # information and will have a different order depending on who is p1 and p2.
514 # That order is stable accross repositories. (data from p1 prevails)
515
476 516 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mAEm-0")'
477 517 A f
478 518 d
479 519 R b
480 520 R d
481 521 $ hg status --copies --rev 'desc("i-2")' --rev 'desc("mEAm-0")'
482 522 A f
483 d
523 d (filelog !)
524 b (no-filelog !)
484 525 R b
485 526 R d
486 527 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mAEm-0")'
487 528 A f
488 529 a
489 530 R a
490 531 R b
491 532 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mEAm-0")'
492 533 A f
493 a
534 a (filelog !)
535 b (no-filelog !)
494 536 R a
495 537 R b
496 538
497 539
498 540 Note:
499 541 | In this case, one of the merge wrongly record a merge while there is none.
500 542 | This lead to bad copy tracing information to be dug up.
501 543
502 544
503 545 Merge:
504 546 - one with change to an unrelated file (b)
505 547 - one overwriting a file (d) with a rename (from h to i to d)
506 548
507 549 $ hg up 'desc("i-2")'
508 550 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
509 551 $ hg mv h i
510 552 $ hg commit -m "f-1: rename h -> i"
511 553 created new head
512 554 $ hg mv --force i d
513 555 $ hg commit -m "f-2: rename i -> d"
514 556 $ hg debugindex d
515 557 rev linkrev nodeid p1 p2
516 558 0 2 01c2f5eabdc4 000000000000 000000000000
517 559 1 8 b004912a8510 000000000000 000000000000
518 560 2 22 c72365ee036f 000000000000 000000000000
519 561 $ hg up 'desc("b-1")'
520 562 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
521 563 $ hg merge 'desc("f-2")'
522 564 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
523 565 (branch merge, don't forget to commit)
524 566 $ hg ci -m 'mBFm-0 simple merge - one way'
525 567 $ hg up 'desc("f-2")'
526 568 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
527 569 $ hg merge 'desc("b-1")'
528 570 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
529 571 (branch merge, don't forget to commit)
530 572 $ hg ci -m 'mFBm-0 simple merge - the other way'
531 573 created new head
532 574 $ hg log -G --rev '::(desc("mBFm")+desc("mFBm"))'
533 575 @ 24 mFBm-0 simple merge - the other way
534 576 |\
535 577 +---o 23 mBFm-0 simple merge - one way
536 578 | |/
537 579 | o 22 f-2: rename i -> d
538 580 | |
539 581 | o 21 f-1: rename h -> i
540 582 | |
541 583 o | 5 b-1: b update
542 584 |/
543 585 o 2 i-2: c -move-> d
544 586 |
545 587 o 1 i-1: a -move-> c
546 588 |
547 589 o 0 i-0 initial commit: a b h
548 590
549 591 The overwriting should take over. However, the behavior is currently buggy
550 592
551 593 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mBFm-0")'
552 594 M b
553 595 A d
554 596 h
555 597 h (false !)
556 598 R a
557 599 R h
558 600 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mFBm-0")'
559 601 M b
560 602 A d
561 603 h
562 604 R a
563 605 R h
564 606 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mBFm-0")'
565 607 M d
608 h (no-filelog !)
566 609 R h
567 610 $ hg status --copies --rev 'desc("f-2")' --rev 'desc("mBFm-0")'
568 611 M b
569 612 $ hg status --copies --rev 'desc("f-1")' --rev 'desc("mBFm-0")'
570 613 M b
571 614 M d
615 i (no-filelog !)
572 616 R i
573 617 $ hg status --copies --rev 'desc("b-1")' --rev 'desc("mFBm-0")'
574 618 M d
619 h (no-filelog !)
575 620 R h
576 621 $ hg status --copies --rev 'desc("f-2")' --rev 'desc("mFBm-0")'
577 622 M b
578 623 $ hg status --copies --rev 'desc("f-1")' --rev 'desc("mFBm-0")'
579 624 M b
580 625 M d
626 i (no-filelog !)
581 627 R i
582 628
583 629 The following graphlog is wrong, the "a -> c -> d" chain was overwritten and should not appear.
584 630
585 631 $ hg log -Gfr 'desc("mBFm-0")' d
586 632 o 22 f-2: rename i -> d
587 633 |
588 634 o 21 f-1: rename h -> i
589 635 :
590 636 o 0 i-0 initial commit: a b h
591 637
592 638
593 639 The following output is correct.
594 640
595 641 $ hg log -Gfr 'desc("mFBm-0")' d
596 642 o 22 f-2: rename i -> d
597 643 |
598 644 o 21 f-1: rename h -> i
599 645 :
600 646 o 0 i-0 initial commit: a b h
601 647
602 648
603 649
604 650 Merge:
605 651 - one with change to a file
606 652 - one deleting and recreating the file
607 653
608 654 Unlike in the 'BD/DB' cases, an actual merge happened here. So we should
609 655 consider history and rename on both branch of the merge.
610 656
611 657 $ hg up 'desc("i-2")'
612 658 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
613 659 $ echo "some update" >> d
614 660 $ hg commit -m "g-1: update d"
615 661 created new head
616 662 $ hg up 'desc("d-2")'
617 663 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
618 664 $ hg merge 'desc("g-1")' --tool :union
619 665 merging d
620 666 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
621 667 (branch merge, don't forget to commit)
622 668 $ hg ci -m 'mDGm-0 simple merge - one way'
623 669 $ hg up 'desc("g-1")'
624 670 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
625 671 $ hg merge 'desc("d-2")' --tool :union
626 672 merging d
627 673 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
628 674 (branch merge, don't forget to commit)
629 675 $ hg ci -m 'mGDm-0 simple merge - the other way'
630 676 created new head
631 677 $ hg log -G --rev '::(desc("mDGm")+desc("mGDm"))'
632 678 @ 27 mGDm-0 simple merge - the other way
633 679 |\
634 680 +---o 26 mDGm-0 simple merge - one way
635 681 | |/
636 682 | o 25 g-1: update d
637 683 | |
638 684 o | 8 d-2 re-add d
639 685 | |
640 686 o | 7 d-1 delete d
641 687 |/
642 688 o 2 i-2: c -move-> d
643 689 |
644 690 o 1 i-1: a -move-> c
645 691 |
646 692 o 0 i-0 initial commit: a b h
647 693
694 One side of the merge have a long history with rename. The other side of the
695 merge point to a new file with a smaller history. Each side is "valid".
696
697 (and again the filelog based algorithm only explore one, with a pick based on
698 revision numbers)
699
648 700 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mDGm-0")'
649 701 A d
650 a
702 a (filelog !)
651 703 R a
652 704 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mGDm-0")'
653 705 A d
654 706 a
655 707 R a
656 708 $ hg status --copies --rev 'desc("d-2")' --rev 'desc("mDGm-0")'
657 709 M d
658 710 $ hg status --copies --rev 'desc("d-2")' --rev 'desc("mGDm-0")'
659 711 M d
660 712 $ hg status --copies --rev 'desc("g-1")' --rev 'desc("mDGm-0")'
661 713 M d
662 714 $ hg status --copies --rev 'desc("g-1")' --rev 'desc("mGDm-0")'
663 715 M d
664 716
665 717 $ hg log -Gfr 'desc("mDGm-0")' d
666 718 o 26 mDGm-0 simple merge - one way
667 719 |\
668 720 | o 25 g-1: update d
669 721 | |
670 722 o | 8 d-2 re-add d
671 723 |/
672 724 o 2 i-2: c -move-> d
673 725 |
674 726 o 1 i-1: a -move-> c
675 727 |
676 728 o 0 i-0 initial commit: a b h
677 729
678 730
679 731
680 732 $ hg log -Gfr 'desc("mDGm-0")' d
681 733 o 26 mDGm-0 simple merge - one way
682 734 |\
683 735 | o 25 g-1: update d
684 736 | |
685 737 o | 8 d-2 re-add d
686 738 |/
687 739 o 2 i-2: c -move-> d
688 740 |
689 741 o 1 i-1: a -move-> c
690 742 |
691 743 o 0 i-0 initial commit: a b h
692 744
693 745
694 746
695 747 Merge:
696 748 - one with change to a file (d)
697 749 - one overwriting that file with a rename (from h to i, to d)
698 750
699 751 This case is similar to BF/FB, but an actual merge happens, so both side of the
700 752 history are relevant.
701 753
702 754 Note:
703 755 | In this case, the merge get conflicting information since on one side we have
704 756 | "a -> c -> d". and one the other one we have "h -> i -> d".
705 757 |
706 758 | The current code arbitrarily pick one side
707 759
708 760 $ hg up 'desc("f-2")'
709 761 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
710 762 $ hg merge 'desc("g-1")' --tool :union
711 763 merging d
712 764 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
713 765 (branch merge, don't forget to commit)
714 766 $ hg ci -m 'mFGm-0 simple merge - one way'
715 767 created new head
716 768 $ hg up 'desc("g-1")'
717 769 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
718 770 $ hg merge 'desc("f-2")' --tool :union
719 771 merging d
720 772 0 files updated, 1 files merged, 1 files removed, 0 files unresolved
721 773 (branch merge, don't forget to commit)
722 774 $ hg ci -m 'mGFm-0 simple merge - the other way'
723 775 created new head
724 776 $ hg log -G --rev '::(desc("mGFm")+desc("mFGm"))'
725 777 @ 29 mGFm-0 simple merge - the other way
726 778 |\
727 779 +---o 28 mFGm-0 simple merge - one way
728 780 | |/
729 781 | o 25 g-1: update d
730 782 | |
731 783 o | 22 f-2: rename i -> d
732 784 | |
733 785 o | 21 f-1: rename h -> i
734 786 |/
735 787 o 2 i-2: c -move-> d
736 788 |
737 789 o 1 i-1: a -move-> c
738 790 |
739 791 o 0 i-0 initial commit: a b h
740 792
741 793 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mFGm-0")'
742 794 A d
743 a
795 h (no-filelog !)
796 a (filelog !)
744 797 R a
745 798 R h
746 799 $ hg status --copies --rev 'desc("i-0")' --rev 'desc("mGFm-0")'
747 800 A d
748 801 a
749 802 R a
750 803 R h
751 804 $ hg status --copies --rev 'desc("f-2")' --rev 'desc("mFGm-0")'
752 805 M d
753 806 $ hg status --copies --rev 'desc("f-2")' --rev 'desc("mGFm-0")'
754 807 M d
755 808 $ hg status --copies --rev 'desc("f-1")' --rev 'desc("mFGm-0")'
756 809 M d
810 i (no-filelog !)
757 811 R i
758 812 $ hg status --copies --rev 'desc("f-1")' --rev 'desc("mGFm-0")'
759 813 M d
814 i (no-filelog !)
760 815 R i
761 816 $ hg status --copies --rev 'desc("g-1")' --rev 'desc("mFGm-0")'
762 817 M d
818 h (no-filelog !)
763 819 R h
764 820 $ hg status --copies --rev 'desc("g-1")' --rev 'desc("mGFm-0")'
765 821 M d
822 h (no-filelog !)
766 823 R h
767 824
768 825 $ hg log -Gfr 'desc("mFGm-0")' d
769 826 o 28 mFGm-0 simple merge - one way
770 827 |\
771 828 | o 25 g-1: update d
772 829 | |
773 830 o | 22 f-2: rename i -> d
774 831 | |
775 832 o | 21 f-1: rename h -> i
776 833 |/
777 834 o 2 i-2: c -move-> d
778 835 |
779 836 o 1 i-1: a -move-> c
780 837 |
781 838 o 0 i-0 initial commit: a b h
782 839
783 840
784 841 $ hg log -Gfr 'desc("mGFm-0")' d
785 842 @ 29 mGFm-0 simple merge - the other way
786 843 |\
787 844 | o 25 g-1: update d
788 845 | |
789 846 o | 22 f-2: rename i -> d
790 847 | |
791 848 o | 21 f-1: rename h -> i
792 849 |/
793 850 o 2 i-2: c -move-> d
794 851 |
795 852 o 1 i-1: a -move-> c
796 853 |
797 854 o 0 i-0 initial commit: a b h
798 855
General Comments 0
You need to be logged in to leave comments. Login now