##// END OF EJS Templates
Fix copy detection corner case...
Matt Mackall -
r3875:c0a12e64 default
parent child Browse files
Show More
@@ -1,495 +1,495 b''
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import gettext as _
10 10 from demandload import *
11 11 demandload(globals(), "errno util os tempfile")
12 12
13 13 def filemerge(repo, fw, fo, wctx, mctx):
14 14 """perform a 3-way merge in the working directory
15 15
16 16 fw = filename in the working directory
17 17 fo = filename in other parent
18 18 wctx, mctx = working and merge changecontexts
19 19 """
20 20
21 21 def temp(prefix, ctx):
22 22 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
23 23 (fd, name) = tempfile.mkstemp(prefix=pre)
24 24 f = os.fdopen(fd, "wb")
25 25 repo.wwrite(ctx.path(), ctx.data(), f)
26 26 f.close()
27 27 return name
28 28
29 29 fcm = wctx.filectx(fw)
30 30 fco = mctx.filectx(fo)
31 31
32 32 if not fco.cmp(fcm.data()): # files identical?
33 33 return None
34 34
35 35 fca = fcm.ancestor(fco)
36 36 if not fca:
37 37 fca = repo.filectx(fw, fileid=nullrev)
38 38 a = repo.wjoin(fw)
39 39 b = temp("base", fca)
40 40 c = temp("other", fco)
41 41
42 42 if fw != fo:
43 43 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
44 44 else:
45 45 repo.ui.status(_("merging %s\n") % fw)
46 46
47 47 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
48 48
49 49 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
50 50 or "hgmerge")
51 51 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
52 52 environ={'HG_FILE': fw,
53 53 'HG_MY_NODE': str(wctx.parents()[0]),
54 54 'HG_OTHER_NODE': str(mctx)})
55 55 if r:
56 56 repo.ui.warn(_("merging %s failed!\n") % fw)
57 57
58 58 os.unlink(b)
59 59 os.unlink(c)
60 60 return r
61 61
62 62 def checkunknown(wctx, mctx):
63 63 "check for collisions between unknown files and files in mctx"
64 64 man = mctx.manifest()
65 65 for f in wctx.unknown():
66 66 if f in man:
67 67 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
68 68 raise util.Abort(_("untracked local file '%s' differs"\
69 69 " from remote version") % f)
70 70
71 71 def checkcollision(mctx):
72 72 "check for case folding collisions in the destination context"
73 73 folded = {}
74 74 for fn in mctx.manifest():
75 75 fold = fn.lower()
76 76 if fold in folded:
77 77 raise util.Abort(_("case-folding collision between %s and %s")
78 78 % (fn, folded[fold]))
79 79 folded[fold] = fn
80 80
81 81 def forgetremoved(wctx, mctx):
82 82 """
83 83 Forget removed files
84 84
85 85 If we're jumping between revisions (as opposed to merging), and if
86 86 neither the working directory nor the target rev has the file,
87 87 then we need to remove it from the dirstate, to prevent the
88 88 dirstate from listing the file when it is no longer in the
89 89 manifest.
90 90 """
91 91
92 92 action = []
93 93 man = mctx.manifest()
94 94 for f in wctx.deleted() + wctx.removed():
95 95 if f not in man:
96 96 action.append((f, "f"))
97 97
98 98 return action
99 99
100 100 def findcopies(repo, m1, m2, ma, limit):
101 101 """
102 102 Find moves and copies between m1 and m2 back to limit linkrev
103 103 """
104 104
105 105 def findold(fctx):
106 106 "find files that path was copied from, back to linkrev limit"
107 107 old = {}
108 108 orig = fctx.path()
109 109 visit = [fctx]
110 110 while visit:
111 111 fc = visit.pop()
112 if fc.path() != orig and fc.path() not in old:
113 old[fc.path()] = 1
112 114 if fc.rev() < limit:
113 115 continue
114 if fc.path() != orig and fc.path() not in old:
115 old[fc.path()] = 1
116 116 visit += fc.parents()
117 117
118 118 old = old.keys()
119 119 old.sort()
120 120 return old
121 121
122 122 def nonoverlap(d1, d2, d3):
123 123 "Return list of elements in d1 not in d2 or d3"
124 124 l = [d for d in d1 if d not in d3 and d not in d2]
125 125 l.sort()
126 126 return l
127 127
128 128 def checkcopies(c, man):
129 129 '''check possible copies for filectx c'''
130 130 for of in findold(c):
131 131 if of not in man:
132 132 return
133 133 c2 = ctx(of, man[of])
134 134 ca = c.ancestor(c2)
135 135 if not ca: # unrelated
136 136 return
137 137 if ca.path() == c.path() or ca.path() == c2.path():
138 138 fullcopy[c.path()] = of
139 139 if c == ca or c2 == ca: # no merge needed, ignore copy
140 140 return
141 141 copy[c.path()] = of
142 142
143 143 def dirs(files):
144 144 d = {}
145 145 for f in files:
146 146 d[os.path.dirname(f)] = True
147 147 return d
148 148
149 149 if not repo.ui.configbool("merge", "followcopies", True):
150 150 return {}
151 151
152 152 # avoid silly behavior for update from empty dir
153 153 if not m1 or not m2 or not ma:
154 154 return {}
155 155
156 156 dcopies = repo.dirstate.copies()
157 157 copy = {}
158 158 fullcopy = {}
159 159 u1 = nonoverlap(m1, m2, ma)
160 160 u2 = nonoverlap(m2, m1, ma)
161 161 ctx = util.cachefunc(lambda f, n: repo.filectx(f, fileid=n[:20]))
162 162
163 163 for f in u1:
164 164 checkcopies(ctx(dcopies.get(f, f), m1[f]), m2)
165 165
166 166 for f in u2:
167 167 checkcopies(ctx(f, m2[f]), m1)
168 168
169 169 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
170 170 return copy
171 171
172 172 # generate a directory move map
173 173 d1, d2 = dirs(m1), dirs(m2)
174 174 invalid = {}
175 175 dirmove = {}
176 176
177 177 for dst, src in fullcopy.items():
178 178 dsrc, ddst = os.path.dirname(src), os.path.dirname(dst)
179 179 if dsrc in invalid:
180 180 continue
181 181 elif (dsrc in d1 and ddst in d1) or (dsrc in d2 and ddst in d2):
182 182 invalid[dsrc] = True
183 183 elif dsrc in dirmove and dirmove[dsrc] != ddst:
184 184 invalid[dsrc] = True
185 185 del dirmove[dsrc]
186 186 else:
187 187 dirmove[dsrc] = ddst
188 188
189 189 del d1, d2, invalid
190 190
191 191 if not dirmove:
192 192 return copy
193 193
194 194 # check unaccounted nonoverlapping files
195 195 for f in u1 + u2:
196 196 if f not in fullcopy:
197 197 d = os.path.dirname(f)
198 198 if d in dirmove:
199 199 copy[f] = dirmove[d] + "/" + os.path.basename(f)
200 200
201 201 return copy
202 202
203 203 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
204 204 """
205 205 Merge p1 and p2 with ancestor ma and generate merge action list
206 206
207 207 overwrite = whether we clobber working files
208 208 partial = function to filter file lists
209 209 """
210 210
211 211 repo.ui.note(_("resolving manifests\n"))
212 212 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
213 213 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
214 214
215 215 m1 = p1.manifest()
216 216 m2 = p2.manifest()
217 217 ma = pa.manifest()
218 218 backwards = (pa == p2)
219 219 action = []
220 220 copy = {}
221 221
222 222 def fmerge(f, f2=None, fa=None):
223 223 """merge executable flags"""
224 224 if not f2:
225 225 f2 = f
226 226 fa = f
227 227 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
228 228 return ((a^b) | (a^c)) ^ a
229 229
230 230 def act(msg, m, f, *args):
231 231 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
232 232 action.append((f, m) + args)
233 233
234 234 if not (backwards or overwrite):
235 235 copy = findcopies(repo, m1, m2, ma, pa.rev())
236 236 copied = dict.fromkeys(copy.values())
237 237
238 238 # Compare manifests
239 239 for f, n in m1.iteritems():
240 240 if partial and not partial(f):
241 241 continue
242 242 if f in m2:
243 243 # are files different?
244 244 if n != m2[f]:
245 245 a = ma.get(f, nullid)
246 246 # are both different from the ancestor?
247 247 if not overwrite and n != a and m2[f] != a:
248 248 act("versions differ", "m", f, f, f, fmerge(f), False)
249 249 # are we clobbering?
250 250 # is remote's version newer?
251 251 # or are we going back in time and clean?
252 252 elif overwrite or m2[f] != a or (backwards and not n[20:]):
253 253 act("remote is newer", "g", f, m2.execf(f))
254 254 # local is newer, not overwrite, check mode bits
255 255 elif fmerge(f) != m1.execf(f):
256 256 act("update permissions", "e", f, m2.execf(f))
257 257 # contents same, check mode bits
258 258 elif m1.execf(f) != m2.execf(f):
259 259 if overwrite or fmerge(f) != m1.execf(f):
260 260 act("update permissions", "e", f, m2.execf(f))
261 261 elif f in copied:
262 262 continue
263 263 elif f in copy:
264 264 f2 = copy[f]
265 265 if f2 not in m2: # directory rename
266 266 act("remote renamed directory to " + f2, "d",
267 267 f, None, f2, m1.execf(f))
268 268 elif f2 in m1: # case 2 A,B/B/B
269 269 act("local copied to " + f2, "m",
270 270 f, f2, f, fmerge(f, f2, f2), False)
271 271 else: # case 4,21 A/B/B
272 272 act("local moved to " + f2, "m",
273 273 f, f2, f, fmerge(f, f2, f2), False)
274 274 elif f in ma:
275 275 if n != ma[f] and not overwrite:
276 276 if repo.ui.prompt(
277 277 (_(" local changed %s which remote deleted\n") % f) +
278 278 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
279 279 act("prompt delete", "r", f)
280 280 else:
281 281 act("other deleted", "r", f)
282 282 else:
283 283 # file is created on branch or in working directory
284 284 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
285 285 act("remote deleted", "r", f)
286 286
287 287 for f, n in m2.iteritems():
288 288 if partial and not partial(f):
289 289 continue
290 290 if f in m1:
291 291 continue
292 292 if f in copied:
293 293 continue
294 294 if f in copy:
295 295 f2 = copy[f]
296 296 if f2 not in m1: # directory rename
297 297 act("local renamed directory to " + f2, "d",
298 298 None, f, f2, m2.execf(f))
299 299 elif f2 in m2: # rename case 1, A/A,B/A
300 300 act("remote copied to " + f, "m",
301 301 f2, f, f, fmerge(f2, f, f2), False)
302 302 else: # case 3,20 A/B/A
303 303 act("remote moved to " + f, "m",
304 304 f2, f, f, fmerge(f2, f, f2), True)
305 305 elif f in ma:
306 306 if overwrite or backwards:
307 307 act("recreating", "g", f, m2.execf(f))
308 308 elif n != ma[f]:
309 309 if repo.ui.prompt(
310 310 (_("remote changed %s which local deleted\n") % f) +
311 311 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
312 312 act("prompt recreating", "g", f, m2.execf(f))
313 313 else:
314 314 act("remote created", "g", f, m2.execf(f))
315 315
316 316 return action
317 317
318 318 def applyupdates(repo, action, wctx, mctx):
319 319 "apply the merge action list to the working directory"
320 320
321 321 updated, merged, removed, unresolved = 0, 0, 0, 0
322 322 action.sort()
323 323 for a in action:
324 324 f, m = a[:2]
325 325 if f and f[0] == "/":
326 326 continue
327 327 if m == "r": # remove
328 328 repo.ui.note(_("removing %s\n") % f)
329 329 util.audit_path(f)
330 330 try:
331 331 util.unlink(repo.wjoin(f))
332 332 except OSError, inst:
333 333 if inst.errno != errno.ENOENT:
334 334 repo.ui.warn(_("update failed to remove %s: %s!\n") %
335 335 (f, inst.strerror))
336 336 removed += 1
337 337 elif m == "m": # merge
338 338 f2, fd, flag, move = a[2:]
339 339 r = filemerge(repo, f, f2, wctx, mctx)
340 340 if r > 0:
341 341 unresolved += 1
342 342 else:
343 343 if r is None:
344 344 updated += 1
345 345 else:
346 346 merged += 1
347 347 if f != fd:
348 348 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
349 349 repo.wwrite(fd, repo.wread(f))
350 350 if move:
351 351 repo.ui.debug(_("removing %s\n") % f)
352 352 os.unlink(repo.wjoin(f))
353 353 util.set_exec(repo.wjoin(fd), flag)
354 354 elif m == "g": # get
355 355 flag = a[2]
356 356 repo.ui.note(_("getting %s\n") % f)
357 357 t = mctx.filectx(f).data()
358 358 repo.wwrite(f, t)
359 359 util.set_exec(repo.wjoin(f), flag)
360 360 updated += 1
361 361 elif m == "d": # directory rename
362 362 f2, fd, flag = a[2:]
363 363 if f:
364 364 repo.ui.note(_("moving %s to %s\n") % (f, fd))
365 365 t = wctx.filectx(f).data()
366 366 repo.wwrite(fd, t)
367 367 util.set_exec(repo.wjoin(fd), flag)
368 368 util.unlink(repo.wjoin(f))
369 369 if f2:
370 370 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
371 371 t = mctx.filectx(f2).data()
372 372 repo.wwrite(fd, t)
373 373 util.set_exec(repo.wjoin(fd), flag)
374 374 updated += 1
375 375 elif m == "e": # exec
376 376 flag = a[2]
377 377 util.set_exec(repo.wjoin(f), flag)
378 378
379 379 return updated, merged, removed, unresolved
380 380
381 381 def recordupdates(repo, action, branchmerge):
382 382 "record merge actions to the dirstate"
383 383
384 384 for a in action:
385 385 f, m = a[:2]
386 386 if m == "r": # remove
387 387 if branchmerge:
388 388 repo.dirstate.update([f], 'r')
389 389 else:
390 390 repo.dirstate.forget([f])
391 391 elif m == "f": # forget
392 392 repo.dirstate.forget([f])
393 393 elif m == "g": # get
394 394 if branchmerge:
395 395 repo.dirstate.update([f], 'n', st_mtime=-1)
396 396 else:
397 397 repo.dirstate.update([f], 'n')
398 398 elif m == "m": # merge
399 399 f2, fd, flag, move = a[2:]
400 400 if branchmerge:
401 401 # We've done a branch merge, mark this file as merged
402 402 # so that we properly record the merger later
403 403 repo.dirstate.update([fd], 'm')
404 404 if f != f2: # copy/rename
405 405 if move:
406 406 repo.dirstate.update([f], 'r')
407 407 if f != fd:
408 408 repo.dirstate.copy(f, fd)
409 409 else:
410 410 repo.dirstate.copy(f2, fd)
411 411 else:
412 412 # We've update-merged a locally modified file, so
413 413 # we set the dirstate to emulate a normal checkout
414 414 # of that file some time in the past. Thus our
415 415 # merge will appear as a normal local file
416 416 # modification.
417 417 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
418 418 if move:
419 419 repo.dirstate.forget([f])
420 420 elif m == "d": # directory rename
421 421 f2, fd, flag = a[2:]
422 422 if branchmerge:
423 423 repo.dirstate.update([fd], 'a')
424 424 if f:
425 425 repo.dirstate.update([f], 'r')
426 426 repo.dirstate.copy(f, fd)
427 427 if f2:
428 428 repo.dirstate.copy(f2, fd)
429 429 else:
430 430 repo.dirstate.update([fd], 'n')
431 431 if f:
432 432 repo.dirstate.forget([f])
433 433
434 434 def update(repo, node, branchmerge, force, partial, wlock):
435 435 """
436 436 Perform a merge between the working directory and the given node
437 437
438 438 branchmerge = whether to merge between branches
439 439 force = whether to force branch merging or file overwriting
440 440 partial = a function to filter file lists (dirstate not updated)
441 441 wlock = working dir lock, if already held
442 442 """
443 443
444 444 if not wlock:
445 445 wlock = repo.wlock()
446 446
447 447 overwrite = force and not branchmerge
448 448 forcemerge = force and branchmerge
449 449 wc = repo.workingctx()
450 450 pl = wc.parents()
451 451 p1, p2 = pl[0], repo.changectx(node)
452 452 pa = p1.ancestor(p2)
453 453 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
454 454
455 455 ### check phase
456 456 if not overwrite and len(pl) > 1:
457 457 raise util.Abort(_("outstanding uncommitted merges"))
458 458 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
459 459 if branchmerge:
460 460 raise util.Abort(_("there is nothing to merge, just use "
461 461 "'hg update' or look at 'hg heads'"))
462 462 elif not (overwrite or branchmerge):
463 463 raise util.Abort(_("update spans branches, use 'hg merge' "
464 464 "or 'hg update -C' to lose changes"))
465 465 if branchmerge and not forcemerge:
466 466 if wc.files():
467 467 raise util.Abort(_("outstanding uncommitted changes"))
468 468
469 469 ### calculate phase
470 470 action = []
471 471 if not force:
472 472 checkunknown(wc, p2)
473 473 if not util.checkfolding(repo.path):
474 474 checkcollision(p2)
475 475 if not branchmerge:
476 476 action += forgetremoved(wc, p2)
477 477 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
478 478
479 479 ### apply phase
480 480 if not branchmerge: # just jump to the new rev
481 481 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
482 482 if not partial:
483 483 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
484 484
485 485 stats = applyupdates(repo, action, wc, p2)
486 486
487 487 if not partial:
488 488 recordupdates(repo, action, branchmerge)
489 489 repo.dirstate.setparents(fp1, fp2)
490 490 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
491 491 if not branchmerge:
492 492 repo.opener("branch", "w").write(p2.branch() + "\n")
493 493
494 494 return stats
495 495
General Comments 0
You need to be logged in to leave comments. Login now