comparison mercurial/copies.py @ 42595:819712deac69

copies: follow copies across merge base without source file (issue6163) As in the previous patch, consider these two histories: @ 4 'rename x to y' | o 3 'add x again' | o 2 'remove x' | | o 1 'modify x' |/ o 0 'add x' @ 4 'rename x to y' | o 3 'add x again' | | o 2 'modify x' | | | o 1 'add x' |/ o 0 'base' We trace copies from the 'modify x' commit to commit 4 by going via the merge base (commit 0). When tracing file 'y' (_tracefile()) in the first case, we immediately find the rename from 'x'. We check to see if 'x' exists in the merge base, which it does, so we consider it a valid copy. In the second case, 'x' does not exist in the merge base, so it's not considered a valid copy. As a workaround, this patch makes it so we also attempt the check in mergecopies's base commit (commit 1 in the second case). That feels pretty ugly to me, but I don't have any better ideas. Note that we actually also check not only that the filename matches, but also that the file's nodeid matches. I don't know why we do that, but it was like that already before I rewrote mergecopies(). That means that the rebase will still fail in cases like this (again, it already failed before my rewrite): @ 4 'rename x to y' | o 3 'add x again with content X2' | o 2 'remove x' | | o 1 'modify x to content X2' |/ o 1 'modify x to content X1' | o 0 'add x with content X0' Differential Revision: https://phab.mercurial-scm.org/D6604
author Martin von Zweigbergk <martinvonz@google.com>
date Fri, 28 Jun 2019 12:59:21 -0700
parents d013099c551b
children 8c5a36805d5d
comparison
equal deleted inserted replaced
42594:d013099c551b 42595:819712deac69
148 t[k] = t[v] 148 t[k] = t[v]
149 else: 149 else:
150 t[k] = v 150 t[k] = v
151 return t 151 return t
152 152
153 def _tracefile(fctx, am, limit): 153 def _tracefile(fctx, am, basemf, limit):
154 """return file context that is the ancestor of fctx present in ancestor 154 """return file context that is the ancestor of fctx present in ancestor
155 manifest am, stopping after the first ancestor lower than limit""" 155 manifest am, stopping after the first ancestor lower than limit"""
156 156
157 for f in fctx.ancestors(): 157 for f in fctx.ancestors():
158 path = f.path() 158 path = f.path()
159 if am.get(path, None) == f.filenode(): 159 if am.get(path, None) == f.filenode():
160 return path
161 if basemf and basemf.get(path, None) == f.filenode():
160 return path 162 return path
161 if not f.isintroducedafter(limit): 163 if not f.isintroducedafter(limit):
162 return None 164 return None
163 165
164 def _dirstatecopies(repo, match=None): 166 def _dirstatecopies(repo, match=None):
181 def usechangesetcentricalgo(repo): 183 def usechangesetcentricalgo(repo):
182 """Checks if we should use changeset-centric copy algorithms""" 184 """Checks if we should use changeset-centric copy algorithms"""
183 return (repo.ui.config('experimental', 'copies.read-from') in 185 return (repo.ui.config('experimental', 'copies.read-from') in
184 ('changeset-only', 'compatibility')) 186 ('changeset-only', 'compatibility'))
185 187
186 def _committedforwardcopies(a, b, match): 188 def _committedforwardcopies(a, b, base, match):
187 """Like _forwardcopies(), but b.rev() cannot be None (working copy)""" 189 """Like _forwardcopies(), but b.rev() cannot be None (working copy)"""
188 # files might have to be traced back to the fctx parent of the last 190 # files might have to be traced back to the fctx parent of the last
189 # one-side-only changeset, but not further back than that 191 # one-side-only changeset, but not further back than that
190 repo = a._repo 192 repo = a._repo
191 193
199 % (a, b)) 201 % (a, b))
200 limit = _findlimit(repo, a, b) 202 limit = _findlimit(repo, a, b)
201 if debug: 203 if debug:
202 dbg('debug.copies: search limit: %d\n' % limit) 204 dbg('debug.copies: search limit: %d\n' % limit)
203 am = a.manifest() 205 am = a.manifest()
206 basemf = None if base is None else base.manifest()
204 207
205 # find where new files came from 208 # find where new files came from
206 # we currently don't try to find where old files went, too expensive 209 # we currently don't try to find where old files went, too expensive
207 # this means we can miss a case like 'hg rm b; hg cp a b' 210 # this means we can miss a case like 'hg rm b; hg cp a b'
208 cm = {} 211 cm = {}
230 fctx = b[f] 233 fctx = b[f]
231 fctx._ancestrycontext = ancestrycontext 234 fctx._ancestrycontext = ancestrycontext
232 235
233 if debug: 236 if debug:
234 start = util.timer() 237 start = util.timer()
235 opath = _tracefile(fctx, am, limit) 238 opath = _tracefile(fctx, am, basemf, limit)
236 if opath: 239 if opath:
237 if debug: 240 if debug:
238 dbg('debug.copies: rename of: %s\n' % opath) 241 dbg('debug.copies: rename of: %s\n' % opath)
239 cm[f] = opath 242 cm[f] = opath
240 if debug: 243 if debug:
309 if f in newcopies: 312 if f in newcopies:
310 del newcopies[f] 313 del newcopies[f]
311 heapq.heappush(work, (c, parent, newcopies)) 314 heapq.heappush(work, (c, parent, newcopies))
312 assert False 315 assert False
313 316
314 def _forwardcopies(a, b, match=None): 317 def _forwardcopies(a, b, base=None, match=None):
315 """find {dst@b: src@a} copy mapping where a is an ancestor of b""" 318 """find {dst@b: src@a} copy mapping where a is an ancestor of b"""
316 319
320 if base is None:
321 base = a
317 match = a.repo().narrowmatch(match) 322 match = a.repo().narrowmatch(match)
318 # check for working copy 323 # check for working copy
319 if b.rev() is None: 324 if b.rev() is None:
320 cm = _committedforwardcopies(a, b.p1(), match) 325 cm = _committedforwardcopies(a, b.p1(), base, match)
321 # combine copies from dirstate if necessary 326 # combine copies from dirstate if necessary
322 copies = _chain(cm, _dirstatecopies(b._repo, match)) 327 copies = _chain(cm, _dirstatecopies(b._repo, match))
323 else: 328 else:
324 copies = _committedforwardcopies(a, b, match) 329 copies = _committedforwardcopies(a, b, base, match)
325 return copies 330 return copies
326 331
327 def _backwardrenames(a, b, match): 332 def _backwardrenames(a, b, match):
328 if a._repo.ui.config('experimental', 'copytrace') == 'off': 333 if a._repo.ui.config('experimental', 'copytrace') == 'off':
329 return {} 334 return {}
367 repo.ui.debug('debug.copies: search mode: backward\n') 372 repo.ui.debug('debug.copies: search mode: backward\n')
368 copies = _backwardrenames(x, y, match=match) 373 copies = _backwardrenames(x, y, match=match)
369 else: 374 else:
370 if debug: 375 if debug:
371 repo.ui.debug('debug.copies: search mode: combined\n') 376 repo.ui.debug('debug.copies: search mode: combined\n')
377 base = None
378 if a.rev() != node.nullrev:
379 base = x
372 copies = _chain(_backwardrenames(x, a, match=match), 380 copies = _chain(_backwardrenames(x, a, match=match),
373 _forwardcopies(a, y, match=match)) 381 _forwardcopies(a, y, base, match=match))
374 _filter(x, y, copies) 382 _filter(x, y, copies)
375 return copies 383 return copies
376 384
377 def mergecopies(repo, c1, c2, base): 385 def mergecopies(repo, c1, c2, base):
378 """ 386 """