##// END OF EJS Templates
copies: introduce getdstfctx...
Stanislau Hlebik -
r32565:5313d980 default
parent child Browse files
Show More
@@ -1,716 +1,717 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 heapq
11 11
12 12 from . import (
13 13 node,
14 14 pathutil,
15 15 scmutil,
16 16 util,
17 17 )
18 18
19 19 def _findlimit(repo, a, b):
20 20 """
21 21 Find the last revision that needs to be checked to ensure that a full
22 22 transitive closure for file copies can be properly calculated.
23 23 Generally, this means finding the earliest revision number that's an
24 24 ancestor of a or b but not both, except when a or b is a direct descendent
25 25 of the other, in which case we can return the minimum revnum of a and b.
26 26 None if no such revision exists.
27 27 """
28 28
29 29 # basic idea:
30 30 # - mark a and b with different sides
31 31 # - if a parent's children are all on the same side, the parent is
32 32 # on that side, otherwise it is on no side
33 33 # - walk the graph in topological order with the help of a heap;
34 34 # - add unseen parents to side map
35 35 # - clear side of any parent that has children on different sides
36 36 # - track number of interesting revs that might still be on a side
37 37 # - track the lowest interesting rev seen
38 38 # - quit when interesting revs is zero
39 39
40 40 cl = repo.changelog
41 41 working = len(cl) # pseudo rev for the working directory
42 42 if a is None:
43 43 a = working
44 44 if b is None:
45 45 b = working
46 46
47 47 side = {a: -1, b: 1}
48 48 visit = [-a, -b]
49 49 heapq.heapify(visit)
50 50 interesting = len(visit)
51 51 hascommonancestor = False
52 52 limit = working
53 53
54 54 while interesting:
55 55 r = -heapq.heappop(visit)
56 56 if r == working:
57 57 parents = [cl.rev(p) for p in repo.dirstate.parents()]
58 58 else:
59 59 parents = cl.parentrevs(r)
60 60 for p in parents:
61 61 if p < 0:
62 62 continue
63 63 if p not in side:
64 64 # first time we see p; add it to visit
65 65 side[p] = side[r]
66 66 if side[p]:
67 67 interesting += 1
68 68 heapq.heappush(visit, -p)
69 69 elif side[p] and side[p] != side[r]:
70 70 # p was interesting but now we know better
71 71 side[p] = 0
72 72 interesting -= 1
73 73 hascommonancestor = True
74 74 if side[r]:
75 75 limit = r # lowest rev visited
76 76 interesting -= 1
77 77
78 78 if not hascommonancestor:
79 79 return None
80 80
81 81 # Consider the following flow (see test-commit-amend.t under issue4405):
82 82 # 1/ File 'a0' committed
83 83 # 2/ File renamed from 'a0' to 'a1' in a new commit (call it 'a1')
84 84 # 3/ Move back to first commit
85 85 # 4/ Create a new commit via revert to contents of 'a1' (call it 'a1-amend')
86 86 # 5/ Rename file from 'a1' to 'a2' and commit --amend 'a1-msg'
87 87 #
88 88 # During the amend in step five, we will be in this state:
89 89 #
90 90 # @ 3 temporary amend commit for a1-amend
91 91 # |
92 92 # o 2 a1-amend
93 93 # |
94 94 # | o 1 a1
95 95 # |/
96 96 # o 0 a0
97 97 #
98 98 # When _findlimit is called, a and b are revs 3 and 0, so limit will be 2,
99 99 # yet the filelog has the copy information in rev 1 and we will not look
100 100 # back far enough unless we also look at the a and b as candidates.
101 101 # This only occurs when a is a descendent of b or visa-versa.
102 102 return min(limit, a, b)
103 103
104 104 def _chain(src, dst, a, b):
105 105 '''chain two sets of copies a->b'''
106 106 t = a.copy()
107 107 for k, v in b.iteritems():
108 108 if v in t:
109 109 # found a chain
110 110 if t[v] != k:
111 111 # file wasn't renamed back to itself
112 112 t[k] = t[v]
113 113 if v not in dst:
114 114 # chain was a rename, not a copy
115 115 del t[v]
116 116 if v in src:
117 117 # file is a copy of an existing file
118 118 t[k] = v
119 119
120 120 # remove criss-crossed copies
121 121 for k, v in t.items():
122 122 if k in src and v in dst:
123 123 del t[k]
124 124
125 125 return t
126 126
127 127 def _tracefile(fctx, am, limit=-1):
128 128 '''return file context that is the ancestor of fctx present in ancestor
129 129 manifest am, stopping after the first ancestor lower than limit'''
130 130
131 131 for f in fctx.ancestors():
132 132 if am.get(f.path(), None) == f.filenode():
133 133 return f
134 134 if limit >= 0 and f.linkrev() < limit and f.rev() < limit:
135 135 return None
136 136
137 137 def _dirstatecopies(d):
138 138 ds = d._repo.dirstate
139 139 c = ds.copies().copy()
140 140 for k in c.keys():
141 141 if ds[k] not in 'anm':
142 142 del c[k]
143 143 return c
144 144
145 145 def _computeforwardmissing(a, b, match=None):
146 146 """Computes which files are in b but not a.
147 147 This is its own function so extensions can easily wrap this call to see what
148 148 files _forwardcopies is about to process.
149 149 """
150 150 ma = a.manifest()
151 151 mb = b.manifest()
152 152 return mb.filesnotin(ma, match=match)
153 153
154 154 def _forwardcopies(a, b, match=None):
155 155 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
156 156
157 157 # check for working copy
158 158 w = None
159 159 if b.rev() is None:
160 160 w = b
161 161 b = w.p1()
162 162 if a == b:
163 163 # short-circuit to avoid issues with merge states
164 164 return _dirstatecopies(w)
165 165
166 166 # files might have to be traced back to the fctx parent of the last
167 167 # one-side-only changeset, but not further back than that
168 168 limit = _findlimit(a._repo, a.rev(), b.rev())
169 169 if limit is None:
170 170 limit = -1
171 171 am = a.manifest()
172 172
173 173 # find where new files came from
174 174 # we currently don't try to find where old files went, too expensive
175 175 # this means we can miss a case like 'hg rm b; hg cp a b'
176 176 cm = {}
177 177
178 178 # Computing the forward missing is quite expensive on large manifests, since
179 179 # it compares the entire manifests. We can optimize it in the common use
180 180 # case of computing what copies are in a commit versus its parent (like
181 181 # during a rebase or histedit). Note, we exclude merge commits from this
182 182 # optimization, since the ctx.files() for a merge commit is not correct for
183 183 # this comparison.
184 184 forwardmissingmatch = match
185 185 if not match and b.p1() == a and b.p2().node() == node.nullid:
186 186 forwardmissingmatch = scmutil.matchfiles(a._repo, b.files())
187 187 missing = _computeforwardmissing(a, b, match=forwardmissingmatch)
188 188
189 189 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
190 190 for f in missing:
191 191 fctx = b[f]
192 192 fctx._ancestrycontext = ancestrycontext
193 193 ofctx = _tracefile(fctx, am, limit)
194 194 if ofctx:
195 195 cm[f] = ofctx.path()
196 196
197 197 # combine copies from dirstate if necessary
198 198 if w is not None:
199 199 cm = _chain(a, w, cm, _dirstatecopies(w))
200 200
201 201 return cm
202 202
203 203 def _backwardrenames(a, b):
204 204 if a._repo.ui.configbool('experimental', 'disablecopytrace'):
205 205 return {}
206 206
207 207 # Even though we're not taking copies into account, 1:n rename situations
208 208 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
209 209 # arbitrarily pick one of the renames.
210 210 f = _forwardcopies(b, a)
211 211 r = {}
212 212 for k, v in sorted(f.iteritems()):
213 213 # remove copies
214 214 if v in a:
215 215 continue
216 216 r[v] = k
217 217 return r
218 218
219 219 def pathcopies(x, y, match=None):
220 220 '''find {dst@y: src@x} copy mapping for directed compare'''
221 221 if x == y or not x or not y:
222 222 return {}
223 223 a = y.ancestor(x)
224 224 if a == x:
225 225 return _forwardcopies(x, y, match=match)
226 226 if a == y:
227 227 return _backwardrenames(x, y)
228 228 return _chain(x, y, _backwardrenames(x, a),
229 229 _forwardcopies(a, y, match=match))
230 230
231 231 def _computenonoverlap(repo, c1, c2, addedinm1, addedinm2, baselabel=''):
232 232 """Computes, based on addedinm1 and addedinm2, the files exclusive to c1
233 233 and c2. This is its own function so extensions can easily wrap this call
234 234 to see what files mergecopies is about to process.
235 235
236 236 Even though c1 and c2 are not used in this function, they are useful in
237 237 other extensions for being able to read the file nodes of the changed files.
238 238
239 239 "baselabel" can be passed to help distinguish the multiple computations
240 240 done in the graft case.
241 241 """
242 242 u1 = sorted(addedinm1 - addedinm2)
243 243 u2 = sorted(addedinm2 - addedinm1)
244 244
245 245 header = " unmatched files in %s"
246 246 if baselabel:
247 247 header += ' (from %s)' % baselabel
248 248 if u1:
249 249 repo.ui.debug("%s:\n %s\n" % (header % 'local', "\n ".join(u1)))
250 250 if u2:
251 251 repo.ui.debug("%s:\n %s\n" % (header % 'other', "\n ".join(u2)))
252 252 return u1, u2
253 253
254 254 def _makegetfctx(ctx):
255 255 """return a 'getfctx' function suitable for _checkcopies usage
256 256
257 257 We have to re-setup the function building 'filectx' for each
258 258 '_checkcopies' to ensure the linkrev adjustment is properly setup for
259 259 each. Linkrev adjustment is important to avoid bug in rename
260 260 detection. Moreover, having a proper '_ancestrycontext' setup ensures
261 261 the performance impact of this adjustment is kept limited. Without it,
262 262 each file could do a full dag traversal making the time complexity of
263 263 the operation explode (see issue4537).
264 264
265 265 This function exists here mostly to limit the impact on stable. Feel
266 266 free to refactor on default.
267 267 """
268 268 rev = ctx.rev()
269 269 repo = ctx._repo
270 270 ac = getattr(ctx, '_ancestrycontext', None)
271 271 if ac is None:
272 272 revs = [rev]
273 273 if rev is None:
274 274 revs = [p.rev() for p in ctx.parents()]
275 275 ac = repo.changelog.ancestors(revs, inclusive=True)
276 276 ctx._ancestrycontext = ac
277 277 def makectx(f, n):
278 278 if n in node.wdirnodes: # in a working context?
279 279 if ctx.rev() is None:
280 280 return ctx.filectx(f)
281 281 return repo[None][f]
282 282 fctx = repo.filectx(f, fileid=n)
283 283 # setup only needed for filectx not create from a changectx
284 284 fctx._ancestrycontext = ac
285 285 fctx._descendantrev = rev
286 286 return fctx
287 287 return util.lrucachefunc(makectx)
288 288
289 289 def _combinecopies(copyfrom, copyto, finalcopy, diverge, incompletediverge):
290 290 """combine partial copy paths"""
291 291 remainder = {}
292 292 for f in copyfrom:
293 293 if f in copyto:
294 294 finalcopy[copyto[f]] = copyfrom[f]
295 295 del copyto[f]
296 296 for f in incompletediverge:
297 297 assert f not in diverge
298 298 ic = incompletediverge[f]
299 299 if ic[0] in copyto:
300 300 diverge[f] = [copyto[ic[0]], ic[1]]
301 301 else:
302 302 remainder[f] = ic
303 303 return remainder
304 304
305 305 def mergecopies(repo, c1, c2, base):
306 306 """
307 307 Find moves and copies between context c1 and c2 that are relevant
308 308 for merging. 'base' will be used as the merge base.
309 309
310 310 Returns five dicts: "copy", "movewithdir", "diverge", "renamedelete" and
311 311 "dirmove".
312 312
313 313 "copy" is a mapping from destination name -> source name,
314 314 where source is in c1 and destination is in c2 or vice-versa.
315 315
316 316 "movewithdir" is a mapping from source name -> destination name,
317 317 where the file at source present in one context but not the other
318 318 needs to be moved to destination by the merge process, because the
319 319 other context moved the directory it is in.
320 320
321 321 "diverge" is a mapping of source name -> list of destination names
322 322 for divergent renames.
323 323
324 324 "renamedelete" is a mapping of source name -> list of destination
325 325 names for files deleted in c1 that were renamed in c2 or vice-versa.
326 326
327 327 "dirmove" is a mapping of detected source dir -> destination dir renames.
328 328 This is needed for handling changes to new files previously grafted into
329 329 renamed directories.
330 330 """
331 331 # avoid silly behavior for update from empty dir
332 332 if not c1 or not c2 or c1 == c2:
333 333 return {}, {}, {}, {}, {}
334 334
335 335 # avoid silly behavior for parent -> working dir
336 336 if c2.node() is None and c1.node() == repo.dirstate.p1():
337 337 return repo.dirstate.copies(), {}, {}, {}, {}
338 338
339 339 # Copy trace disabling is explicitly below the node == p1 logic above
340 340 # because the logic above is required for a simple copy to be kept across a
341 341 # rebase.
342 342 if repo.ui.configbool('experimental', 'disablecopytrace'):
343 343 return {}, {}, {}, {}, {}
344 344
345 345 # In certain scenarios (e.g. graft, update or rebase), base can be
346 346 # overridden We still need to know a real common ancestor in this case We
347 347 # can't just compute _c1.ancestor(_c2) and compare it to ca, because there
348 348 # can be multiple common ancestors, e.g. in case of bidmerge. Because our
349 349 # caller may not know if the revision passed in lieu of the CA is a genuine
350 350 # common ancestor or not without explicitly checking it, it's better to
351 351 # determine that here.
352 352 #
353 353 # base.descendant(wc) and base.descendant(base) are False, work around that
354 354 _c1 = c1.p1() if c1.rev() is None else c1
355 355 _c2 = c2.p1() if c2.rev() is None else c2
356 356 # an endpoint is "dirty" if it isn't a descendant of the merge base
357 357 # if we have a dirty endpoint, we need to trigger graft logic, and also
358 358 # keep track of which endpoint is dirty
359 359 dirtyc1 = not (base == _c1 or base.descendant(_c1))
360 360 dirtyc2 = not (base== _c2 or base.descendant(_c2))
361 361 graft = dirtyc1 or dirtyc2
362 362 tca = base
363 363 if graft:
364 364 tca = _c1.ancestor(_c2)
365 365
366 366 limit = _findlimit(repo, c1.rev(), c2.rev())
367 367 if limit is None:
368 368 # no common ancestor, no copies
369 369 return {}, {}, {}, {}, {}
370 370 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
371 371
372 372 m1 = c1.manifest()
373 373 m2 = c2.manifest()
374 374 mb = base.manifest()
375 375
376 376 # gather data from _checkcopies:
377 377 # - diverge = record all diverges in this dict
378 378 # - copy = record all non-divergent copies in this dict
379 379 # - fullcopy = record all copies in this dict
380 380 # - incomplete = record non-divergent partial copies here
381 381 # - incompletediverge = record divergent partial copies here
382 382 diverge = {} # divergence data is shared
383 383 incompletediverge = {}
384 384 data1 = {'copy': {},
385 385 'fullcopy': {},
386 386 'incomplete': {},
387 387 'diverge': diverge,
388 388 'incompletediverge': incompletediverge,
389 389 }
390 390 data2 = {'copy': {},
391 391 'fullcopy': {},
392 392 'incomplete': {},
393 393 'diverge': diverge,
394 394 'incompletediverge': incompletediverge,
395 395 }
396 396
397 397 # find interesting file sets from manifests
398 398 addedinm1 = m1.filesnotin(mb)
399 399 addedinm2 = m2.filesnotin(mb)
400 400 bothnew = sorted(addedinm1 & addedinm2)
401 401 if tca == base:
402 402 # unmatched file from base
403 403 u1r, u2r = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2)
404 404 u1u, u2u = u1r, u2r
405 405 else:
406 406 # unmatched file from base (DAG rotation in the graft case)
407 407 u1r, u2r = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2,
408 408 baselabel='base')
409 409 # unmatched file from topological common ancestors (no DAG rotation)
410 410 # need to recompute this for directory move handling when grafting
411 411 mta = tca.manifest()
412 412 u1u, u2u = _computenonoverlap(repo, c1, c2, m1.filesnotin(mta),
413 413 m2.filesnotin(mta),
414 414 baselabel='topological common ancestor')
415 415
416 416 for f in u1u:
417 417 _checkcopies(c1, c2, f, base, tca, dirtyc1, limit, data1)
418 418
419 419 for f in u2u:
420 420 _checkcopies(c2, c1, f, base, tca, dirtyc2, limit, data2)
421 421
422 422 copy = dict(data1['copy'].items() + data2['copy'].items())
423 423 fullcopy = dict(data1['fullcopy'].items() + data2['fullcopy'].items())
424 424
425 425 if dirtyc1:
426 426 _combinecopies(data2['incomplete'], data1['incomplete'], copy, diverge,
427 427 incompletediverge)
428 428 else:
429 429 _combinecopies(data1['incomplete'], data2['incomplete'], copy, diverge,
430 430 incompletediverge)
431 431
432 432 renamedelete = {}
433 433 renamedeleteset = set()
434 434 divergeset = set()
435 435 for of, fl in diverge.items():
436 436 if len(fl) == 1 or of in c1 or of in c2:
437 437 del diverge[of] # not actually divergent, or not a rename
438 438 if of not in c1 and of not in c2:
439 439 # renamed on one side, deleted on the other side, but filter
440 440 # out files that have been renamed and then deleted
441 441 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
442 442 renamedeleteset.update(fl) # reverse map for below
443 443 else:
444 444 divergeset.update(fl) # reverse map for below
445 445
446 446 if bothnew:
447 447 repo.ui.debug(" unmatched files new in both:\n %s\n"
448 448 % "\n ".join(bothnew))
449 449 bothdiverge = {}
450 450 bothincompletediverge = {}
451 451 remainder = {}
452 452 both1 = {'copy': {},
453 453 'fullcopy': {},
454 454 'incomplete': {},
455 455 'diverge': bothdiverge,
456 456 'incompletediverge': bothincompletediverge
457 457 }
458 458 both2 = {'copy': {},
459 459 'fullcopy': {},
460 460 'incomplete': {},
461 461 'diverge': bothdiverge,
462 462 'incompletediverge': bothincompletediverge
463 463 }
464 464 for f in bothnew:
465 465 _checkcopies(c1, c2, f, base, tca, dirtyc1, limit, both1)
466 466 _checkcopies(c2, c1, f, base, tca, dirtyc2, limit, both2)
467 467 if dirtyc1:
468 468 # incomplete copies may only be found on the "dirty" side for bothnew
469 469 assert not both2['incomplete']
470 470 remainder = _combinecopies({}, both1['incomplete'], copy, bothdiverge,
471 471 bothincompletediverge)
472 472 elif dirtyc2:
473 473 assert not both1['incomplete']
474 474 remainder = _combinecopies({}, both2['incomplete'], copy, bothdiverge,
475 475 bothincompletediverge)
476 476 else:
477 477 # incomplete copies and divergences can't happen outside grafts
478 478 assert not both1['incomplete']
479 479 assert not both2['incomplete']
480 480 assert not bothincompletediverge
481 481 for f in remainder:
482 482 assert f not in bothdiverge
483 483 ic = remainder[f]
484 484 if ic[0] in (m1 if dirtyc1 else m2):
485 485 # backed-out rename on one side, but watch out for deleted files
486 486 bothdiverge[f] = ic
487 487 for of, fl in bothdiverge.items():
488 488 if len(fl) == 2 and fl[0] == fl[1]:
489 489 copy[fl[0]] = of # not actually divergent, just matching renames
490 490
491 491 if fullcopy and repo.ui.debugflag:
492 492 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
493 493 "% = renamed and deleted):\n")
494 494 for f in sorted(fullcopy):
495 495 note = ""
496 496 if f in copy:
497 497 note += "*"
498 498 if f in divergeset:
499 499 note += "!"
500 500 if f in renamedeleteset:
501 501 note += "%"
502 502 repo.ui.debug(" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
503 503 note))
504 504 del divergeset
505 505
506 506 if not fullcopy:
507 507 return copy, {}, diverge, renamedelete, {}
508 508
509 509 repo.ui.debug(" checking for directory renames\n")
510 510
511 511 # generate a directory move map
512 512 d1, d2 = c1.dirs(), c2.dirs()
513 513 # Hack for adding '', which is not otherwise added, to d1 and d2
514 514 d1.addpath('/')
515 515 d2.addpath('/')
516 516 invalid = set()
517 517 dirmove = {}
518 518
519 519 # examine each file copy for a potential directory move, which is
520 520 # when all the files in a directory are moved to a new directory
521 521 for dst, src in fullcopy.iteritems():
522 522 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
523 523 if dsrc in invalid:
524 524 # already seen to be uninteresting
525 525 continue
526 526 elif dsrc in d1 and ddst in d1:
527 527 # directory wasn't entirely moved locally
528 528 invalid.add(dsrc + "/")
529 529 elif dsrc in d2 and ddst in d2:
530 530 # directory wasn't entirely moved remotely
531 531 invalid.add(dsrc + "/")
532 532 elif dsrc + "/" in dirmove and dirmove[dsrc + "/"] != ddst + "/":
533 533 # files from the same directory moved to two different places
534 534 invalid.add(dsrc + "/")
535 535 else:
536 536 # looks good so far
537 537 dirmove[dsrc + "/"] = ddst + "/"
538 538
539 539 for i in invalid:
540 540 if i in dirmove:
541 541 del dirmove[i]
542 542 del d1, d2, invalid
543 543
544 544 if not dirmove:
545 545 return copy, {}, diverge, renamedelete, {}
546 546
547 547 for d in dirmove:
548 548 repo.ui.debug(" discovered dir src: '%s' -> dst: '%s'\n" %
549 549 (d, dirmove[d]))
550 550
551 551 movewithdir = {}
552 552 # check unaccounted nonoverlapping files against directory moves
553 553 for f in u1r + u2r:
554 554 if f not in fullcopy:
555 555 for d in dirmove:
556 556 if f.startswith(d):
557 557 # new file added in a directory that was moved, move it
558 558 df = dirmove[d] + f[len(d):]
559 559 if df not in copy:
560 560 movewithdir[f] = df
561 561 repo.ui.debug((" pending file src: '%s' -> "
562 562 "dst: '%s'\n") % (f, df))
563 563 break
564 564
565 565 return copy, movewithdir, diverge, renamedelete, dirmove
566 566
567 567 def _related(f1, f2, limit):
568 568 """return True if f1 and f2 filectx have a common ancestor
569 569
570 570 Walk back to common ancestor to see if the two files originate
571 571 from the same file. Since workingfilectx's rev() is None it messes
572 572 up the integer comparison logic, hence the pre-step check for
573 573 None (f1 and f2 can only be workingfilectx's initially).
574 574 """
575 575
576 576 if f1 == f2:
577 577 return f1 # a match
578 578
579 579 g1, g2 = f1.ancestors(), f2.ancestors()
580 580 try:
581 581 f1r, f2r = f1.linkrev(), f2.linkrev()
582 582
583 583 if f1r is None:
584 584 f1 = next(g1)
585 585 if f2r is None:
586 586 f2 = next(g2)
587 587
588 588 while True:
589 589 f1r, f2r = f1.linkrev(), f2.linkrev()
590 590 if f1r > f2r:
591 591 f1 = next(g1)
592 592 elif f2r > f1r:
593 593 f2 = next(g2)
594 594 elif f1 == f2:
595 595 return f1 # a match
596 596 elif f1r == f2r or f1r < limit or f2r < limit:
597 597 return False # copy no longer relevant
598 598 except StopIteration:
599 599 return False
600 600
601 601 def _checkcopies(srcctx, dstctx, f, base, tca, remotebase, limit, data):
602 602 """
603 603 check possible copies of f from msrc to mdst
604 604
605 605 srcctx = starting context for f in msrc
606 606 dstctx = destination context for f in mdst
607 607 f = the filename to check (as in msrc)
608 608 base = the changectx used as a merge base
609 609 tca = topological common ancestor for graft-like scenarios
610 610 remotebase = True if base is outside tca::srcctx, False otherwise
611 611 limit = the rev number to not search beyond
612 612 data = dictionary of dictionary to store copy data. (see mergecopies)
613 613
614 614 note: limit is only an optimization, and there is no guarantee that
615 615 irrelevant revisions will not be limited
616 616 there is no easy way to make this algorithm stop in a guaranteed way
617 617 once it "goes behind a certain revision".
618 618 """
619 619
620 620 msrc = srcctx.manifest()
621 621 mdst = dstctx.manifest()
622 622 mb = base.manifest()
623 623 mta = tca.manifest()
624 624 # Might be true if this call is about finding backward renames,
625 625 # This happens in the case of grafts because the DAG is then rotated.
626 626 # If the file exists in both the base and the source, we are not looking
627 627 # for a rename on the source side, but on the part of the DAG that is
628 628 # traversed backwards.
629 629 #
630 630 # In the case there is both backward and forward renames (before and after
631 631 # the base) this is more complicated as we must detect a divergence.
632 632 # We use 'backwards = False' in that case.
633 633 backwards = not remotebase and base != tca and f in mb
634 634 getsrcfctx = _makegetfctx(srcctx)
635 getdstfctx = _makegetfctx(dstctx)
635 636
636 637 if msrc[f] == mb.get(f) and not remotebase:
637 638 # Nothing to merge
638 639 return
639 640
640 641 of = None
641 642 seen = {f}
642 643 for oc in getsrcfctx(f, msrc[f]).ancestors():
643 644 ocr = oc.linkrev()
644 645 of = oc.path()
645 646 if of in seen:
646 647 # check limit late - grab last rename before
647 648 if ocr < limit:
648 649 break
649 650 continue
650 651 seen.add(of)
651 652
652 653 # remember for dir rename detection
653 654 if backwards:
654 655 data['fullcopy'][of] = f # grafting backwards through renames
655 656 else:
656 657 data['fullcopy'][f] = of
657 658 if of not in mdst:
658 659 continue # no match, keep looking
659 660 if mdst[of] == mb.get(of):
660 661 return # no merge needed, quit early
661 c2 = getsrcfctx(of, mdst[of])
662 c2 = getdstfctx(of, mdst[of])
662 663 # c2 might be a plain new file on added on destination side that is
663 664 # unrelated to the droids we are looking for.
664 665 cr = _related(oc, c2, tca.rev())
665 666 if cr and (of == f or of == c2.path()): # non-divergent
666 667 if backwards:
667 668 data['copy'][of] = f
668 669 elif of in mb:
669 670 data['copy'][f] = of
670 671 elif remotebase: # special case: a <- b <- a -> b "ping-pong" rename
671 672 data['copy'][of] = f
672 673 del data['fullcopy'][f]
673 674 data['fullcopy'][of] = f
674 675 else: # divergence w.r.t. graft CA on one side of topological CA
675 676 for sf in seen:
676 677 if sf in mb:
677 678 assert sf not in data['diverge']
678 679 data['diverge'][sf] = [f, of]
679 680 break
680 681 return
681 682
682 683 if of in mta:
683 684 if backwards or remotebase:
684 685 data['incomplete'][of] = f
685 686 else:
686 687 for sf in seen:
687 688 if sf in mb:
688 689 if tca == base:
689 690 data['diverge'].setdefault(sf, []).append(f)
690 691 else:
691 692 data['incompletediverge'][sf] = [of, f]
692 693 return
693 694
694 695 def duplicatecopies(repo, rev, fromrev, skiprev=None):
695 696 '''reproduce copies from fromrev to rev in the dirstate
696 697
697 698 If skiprev is specified, it's a revision that should be used to
698 699 filter copy records. Any copies that occur between fromrev and
699 700 skiprev will not be duplicated, even if they appear in the set of
700 701 copies between fromrev and rev.
701 702 '''
702 703 exclude = {}
703 704 if (skiprev is not None and
704 705 not repo.ui.configbool('experimental', 'disablecopytrace')):
705 706 # disablecopytrace skips this line, but not the entire function because
706 707 # the line below is O(size of the repo) during a rebase, while the rest
707 708 # of the function is much faster (and is required for carrying copy
708 709 # metadata across the rebase anyway).
709 710 exclude = pathcopies(repo[fromrev], repo[skiprev])
710 711 for dst, src in pathcopies(repo[fromrev], repo[rev]).iteritems():
711 712 # copies.pathcopies returns backward renames, so dst might not
712 713 # actually be in the dirstate
713 714 if dst in exclude:
714 715 continue
715 716 if repo.dirstate[dst] in "nma":
716 717 repo.dirstate.copy(src, dst)
General Comments 0
You need to be logged in to leave comments. Login now