##// END OF EJS Templates
merge: avoid unlinking destination of merge when case changes (issue2715)
Matt Mackall -
r13718:5b3383ea stable
parent child Browse files
Show More
@@ -1,560 +1,561 b''
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 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 node import nullid, nullrev, hex, bin
9 9 from i18n import _
10 10 import util, filemerge, copies, subrepo
11 11 import errno, os, shutil
12 12
13 13 class mergestate(object):
14 14 '''track 3-way merge state of individual files'''
15 15 def __init__(self, repo):
16 16 self._repo = repo
17 17 self._dirty = False
18 18 self._read()
19 19 def reset(self, node=None):
20 20 self._state = {}
21 21 if node:
22 22 self._local = node
23 23 shutil.rmtree(self._repo.join("merge"), True)
24 24 self._dirty = False
25 25 def _read(self):
26 26 self._state = {}
27 27 try:
28 28 f = self._repo.opener("merge/state")
29 29 for i, l in enumerate(f):
30 30 if i == 0:
31 31 self._local = bin(l[:-1])
32 32 else:
33 33 bits = l[:-1].split("\0")
34 34 self._state[bits[0]] = bits[1:]
35 35 f.close()
36 36 except IOError, err:
37 37 if err.errno != errno.ENOENT:
38 38 raise
39 39 self._dirty = False
40 40 def commit(self):
41 41 if self._dirty:
42 42 f = self._repo.opener("merge/state", "w")
43 43 f.write(hex(self._local) + "\n")
44 44 for d, v in self._state.iteritems():
45 45 f.write("\0".join([d] + v) + "\n")
46 46 f.close()
47 47 self._dirty = False
48 48 def add(self, fcl, fco, fca, fd, flags):
49 49 hash = util.sha1(fcl.path()).hexdigest()
50 50 self._repo.opener("merge/" + hash, "w").write(fcl.data())
51 51 self._state[fd] = ['u', hash, fcl.path(), fca.path(),
52 52 hex(fca.filenode()), fco.path(), flags]
53 53 self._dirty = True
54 54 def __contains__(self, dfile):
55 55 return dfile in self._state
56 56 def __getitem__(self, dfile):
57 57 return self._state[dfile][0]
58 58 def __iter__(self):
59 59 l = self._state.keys()
60 60 l.sort()
61 61 for f in l:
62 62 yield f
63 63 def mark(self, dfile, state):
64 64 self._state[dfile][0] = state
65 65 self._dirty = True
66 66 def resolve(self, dfile, wctx, octx):
67 67 if self[dfile] == 'r':
68 68 return 0
69 69 state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
70 70 f = self._repo.opener("merge/" + hash)
71 71 self._repo.wwrite(dfile, f.read(), flags)
72 72 f.close()
73 73 fcd = wctx[dfile]
74 74 fco = octx[ofile]
75 75 fca = self._repo.filectx(afile, fileid=anode)
76 76 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
77 77 if r is None:
78 78 # no real conflict
79 79 del self._state[dfile]
80 80 elif not r:
81 81 self.mark(dfile, 'r')
82 82 return r
83 83
84 84 def _checkunknown(wctx, mctx):
85 85 "check for collisions between unknown files and files in mctx"
86 86 for f in wctx.unknown():
87 87 if f in mctx and mctx[f].cmp(wctx[f]):
88 88 raise util.Abort(_("untracked file in working directory differs"
89 89 " from file in requested revision: '%s'") % f)
90 90
91 91 def _checkcollision(mctx):
92 92 "check for case folding collisions in the destination context"
93 93 folded = {}
94 94 for fn in mctx:
95 95 fold = fn.lower()
96 96 if fold in folded:
97 97 raise util.Abort(_("case-folding collision between %s and %s")
98 98 % (fn, folded[fold]))
99 99 folded[fold] = fn
100 100
101 101 def _forgetremoved(wctx, mctx, branchmerge):
102 102 """
103 103 Forget removed files
104 104
105 105 If we're jumping between revisions (as opposed to merging), and if
106 106 neither the working directory nor the target rev has the file,
107 107 then we need to remove it from the dirstate, to prevent the
108 108 dirstate from listing the file when it is no longer in the
109 109 manifest.
110 110
111 111 If we're merging, and the other revision has removed a file
112 112 that is not present in the working directory, we need to mark it
113 113 as removed.
114 114 """
115 115
116 116 action = []
117 117 state = branchmerge and 'r' or 'f'
118 118 for f in wctx.deleted():
119 119 if f not in mctx:
120 120 action.append((f, state))
121 121
122 122 if not branchmerge:
123 123 for f in wctx.removed():
124 124 if f not in mctx:
125 125 action.append((f, "f"))
126 126
127 127 return action
128 128
129 129 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
130 130 """
131 131 Merge p1 and p2 with ancestor pa and generate merge action list
132 132
133 133 overwrite = whether we clobber working files
134 134 partial = function to filter file lists
135 135 """
136 136
137 137 def fmerge(f, f2, fa):
138 138 """merge flags"""
139 139 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
140 140 if m == n: # flags agree
141 141 return m # unchanged
142 142 if m and n and not a: # flags set, don't agree, differ from parent
143 143 r = repo.ui.promptchoice(
144 144 _(" conflicting flags for %s\n"
145 145 "(n)one, e(x)ec or sym(l)ink?") % f,
146 146 (_("&None"), _("E&xec"), _("Sym&link")), 0)
147 147 if r == 1:
148 148 return "x" # Exec
149 149 if r == 2:
150 150 return "l" # Symlink
151 151 return ""
152 152 if m and m != a: # changed from a to m
153 153 return m
154 154 if n and n != a: # changed from a to n
155 155 return n
156 156 return '' # flag was cleared
157 157
158 158 def act(msg, m, f, *args):
159 159 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
160 160 action.append((f, m) + args)
161 161
162 162 action, copy = [], {}
163 163
164 164 if overwrite:
165 165 pa = p1
166 166 elif pa == p2: # backwards
167 167 pa = p1.p1()
168 168 elif pa and repo.ui.configbool("merge", "followcopies", True):
169 169 dirs = repo.ui.configbool("merge", "followdirs", True)
170 170 copy, diverge = copies.copies(repo, p1, p2, pa, dirs)
171 171 for of, fl in diverge.iteritems():
172 172 act("divergent renames", "dr", of, fl)
173 173
174 174 repo.ui.note(_("resolving manifests\n"))
175 175 repo.ui.debug(" overwrite %s partial %s\n" % (overwrite, bool(partial)))
176 176 repo.ui.debug(" ancestor %s local %s remote %s\n" % (pa, p1, p2))
177 177
178 178 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
179 179 copied = set(copy.values())
180 180
181 181 if '.hgsubstate' in m1:
182 182 # check whether sub state is modified
183 183 for s in p1.substate:
184 184 if p1.sub(s).dirty():
185 185 m1['.hgsubstate'] += "+"
186 186 break
187 187
188 188 # Compare manifests
189 189 for f, n in m1.iteritems():
190 190 if partial and not partial(f):
191 191 continue
192 192 if f in m2:
193 193 rflags = fmerge(f, f, f)
194 194 a = ma.get(f, nullid)
195 195 if n == m2[f] or m2[f] == a: # same or local newer
196 196 # is file locally modified or flags need changing?
197 197 # dirstate flags may need to be made current
198 198 if m1.flags(f) != rflags or n[20:]:
199 199 act("update permissions", "e", f, rflags)
200 200 elif n == a: # remote newer
201 201 act("remote is newer", "g", f, rflags)
202 202 else: # both changed
203 203 act("versions differ", "m", f, f, f, rflags, False)
204 204 elif f in copied: # files we'll deal with on m2 side
205 205 pass
206 206 elif f in copy:
207 207 f2 = copy[f]
208 208 if f2 not in m2: # directory rename
209 209 act("remote renamed directory to " + f2, "d",
210 210 f, None, f2, m1.flags(f))
211 211 else: # case 2 A,B/B/B or case 4,21 A/B/B
212 212 act("local copied/moved to " + f2, "m",
213 213 f, f2, f, fmerge(f, f2, f2), False)
214 214 elif f in ma: # clean, a different, no remote
215 215 if n != ma[f]:
216 216 if repo.ui.promptchoice(
217 217 _(" local changed %s which remote deleted\n"
218 218 "use (c)hanged version or (d)elete?") % f,
219 219 (_("&Changed"), _("&Delete")), 0):
220 220 act("prompt delete", "r", f)
221 221 else:
222 222 act("prompt keep", "a", f)
223 223 elif n[20:] == "a": # added, no remote
224 224 act("remote deleted", "f", f)
225 225 elif n[20:] != "u":
226 226 act("other deleted", "r", f)
227 227
228 228 for f, n in m2.iteritems():
229 229 if partial and not partial(f):
230 230 continue
231 231 if f in m1 or f in copied: # files already visited
232 232 continue
233 233 if f in copy:
234 234 f2 = copy[f]
235 235 if f2 not in m1: # directory rename
236 236 act("local renamed directory to " + f2, "d",
237 237 None, f, f2, m2.flags(f))
238 238 elif f2 in m2: # rename case 1, A/A,B/A
239 239 act("remote copied to " + f, "m",
240 240 f2, f, f, fmerge(f2, f, f2), False)
241 241 else: # case 3,20 A/B/A
242 242 act("remote moved to " + f, "m",
243 243 f2, f, f, fmerge(f2, f, f2), True)
244 244 elif f not in ma:
245 245 act("remote created", "g", f, m2.flags(f))
246 246 elif n != ma[f]:
247 247 if repo.ui.promptchoice(
248 248 _("remote changed %s which local deleted\n"
249 249 "use (c)hanged version or leave (d)eleted?") % f,
250 250 (_("&Changed"), _("&Deleted")), 0) == 0:
251 251 act("prompt recreating", "g", f, m2.flags(f))
252 252
253 253 return action
254 254
255 255 def actionkey(a):
256 256 return a[1] == 'r' and -1 or 0, a
257 257
258 258 def applyupdates(repo, action, wctx, mctx, actx, overwrite):
259 259 """apply the merge action list to the working directory
260 260
261 261 wctx is the working copy context
262 262 mctx is the context to be merged into the working copy
263 263 actx is the context of the common ancestor
264 264
265 265 Return a tuple of counts (updated, merged, removed, unresolved) that
266 266 describes how many files were affected by the update.
267 267 """
268 268
269 269 updated, merged, removed, unresolved = 0, 0, 0, 0
270 270 ms = mergestate(repo)
271 271 ms.reset(wctx.parents()[0].node())
272 272 moves = []
273 273 action.sort(key=actionkey)
274 274 substate = wctx.substate # prime
275 275
276 276 # prescan for merges
277 277 u = repo.ui
278 278 for a in action:
279 279 f, m = a[:2]
280 280 if m == 'm': # merge
281 281 f2, fd, flags, move = a[2:]
282 282 if f == '.hgsubstate': # merged internally
283 283 continue
284 284 repo.ui.debug("preserving %s for resolve of %s\n" % (f, fd))
285 285 fcl = wctx[f]
286 286 fco = mctx[f2]
287 287 if mctx == actx: # backwards, use working dir parent as ancestor
288 288 if fcl.parents():
289 289 fca = fcl.parents()[0]
290 290 else:
291 291 fca = repo.filectx(f, fileid=nullrev)
292 292 else:
293 293 fca = fcl.ancestor(fco, actx)
294 294 if not fca:
295 295 fca = repo.filectx(f, fileid=nullrev)
296 296 ms.add(fcl, fco, fca, fd, flags)
297 297 if f != fd and move:
298 298 moves.append(f)
299 299
300 300 # remove renamed files after safely stored
301 301 for f in moves:
302 302 if os.path.lexists(repo.wjoin(f)):
303 303 repo.ui.debug("removing %s\n" % f)
304 304 os.unlink(repo.wjoin(f))
305 305
306 306 audit_path = util.path_auditor(repo.root)
307 307
308 308 numupdates = len(action)
309 309 for i, a in enumerate(action):
310 310 f, m = a[:2]
311 311 u.progress(_('updating'), i + 1, item=f, total=numupdates,
312 312 unit=_('files'))
313 313 if f and f[0] == "/":
314 314 continue
315 315 if m == "r": # remove
316 316 repo.ui.note(_("removing %s\n") % f)
317 317 audit_path(f)
318 318 if f == '.hgsubstate': # subrepo states need updating
319 319 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
320 320 try:
321 321 util.unlinkpath(repo.wjoin(f))
322 322 except OSError, inst:
323 323 if inst.errno != errno.ENOENT:
324 324 repo.ui.warn(_("update failed to remove %s: %s!\n") %
325 325 (f, inst.strerror))
326 326 removed += 1
327 327 elif m == "m": # merge
328 328 if f == '.hgsubstate': # subrepo states need updating
329 329 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx), overwrite)
330 330 continue
331 331 f2, fd, flags, move = a[2:]
332 332 r = ms.resolve(fd, wctx, mctx)
333 333 if r is not None and r > 0:
334 334 unresolved += 1
335 335 else:
336 336 if r is None:
337 337 updated += 1
338 338 else:
339 339 merged += 1
340 340 util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
341 if f != fd and move and os.path.lexists(repo.wjoin(f)):
341 if (move and repo.dirstate.normalize(fd) != f
342 and os.path.lexists(repo.wjoin(f))):
342 343 repo.ui.debug("removing %s\n" % f)
343 344 os.unlink(repo.wjoin(f))
344 345 elif m == "g": # get
345 346 flags = a[2]
346 347 repo.ui.note(_("getting %s\n") % f)
347 348 t = mctx.filectx(f).data()
348 349 repo.wwrite(f, t, flags)
349 350 t = None
350 351 updated += 1
351 352 if f == '.hgsubstate': # subrepo states need updating
352 353 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
353 354 elif m == "d": # directory rename
354 355 f2, fd, flags = a[2:]
355 356 if f:
356 357 repo.ui.note(_("moving %s to %s\n") % (f, fd))
357 358 t = wctx.filectx(f).data()
358 359 repo.wwrite(fd, t, flags)
359 360 util.unlinkpath(repo.wjoin(f))
360 361 if f2:
361 362 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
362 363 t = mctx.filectx(f2).data()
363 364 repo.wwrite(fd, t, flags)
364 365 updated += 1
365 366 elif m == "dr": # divergent renames
366 367 fl = a[2]
367 368 repo.ui.warn(_("note: possible conflict - %s was renamed "
368 369 "multiple times to:\n") % f)
369 370 for nf in fl:
370 371 repo.ui.warn(" %s\n" % nf)
371 372 elif m == "e": # exec
372 373 flags = a[2]
373 374 util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags)
374 375 ms.commit()
375 376 u.progress(_('updating'), None, total=numupdates, unit=_('files'))
376 377
377 378 return updated, merged, removed, unresolved
378 379
379 380 def recordupdates(repo, action, branchmerge):
380 381 "record merge actions to the dirstate"
381 382
382 383 for a in action:
383 384 f, m = a[:2]
384 385 if m == "r": # remove
385 386 if branchmerge:
386 387 repo.dirstate.remove(f)
387 388 else:
388 389 repo.dirstate.forget(f)
389 390 elif m == "a": # re-add
390 391 if not branchmerge:
391 392 repo.dirstate.add(f)
392 393 elif m == "f": # forget
393 394 repo.dirstate.forget(f)
394 395 elif m == "e": # exec change
395 396 repo.dirstate.normallookup(f)
396 397 elif m == "g": # get
397 398 if branchmerge:
398 399 repo.dirstate.otherparent(f)
399 400 else:
400 401 repo.dirstate.normal(f)
401 402 elif m == "m": # merge
402 403 f2, fd, flag, move = a[2:]
403 404 if branchmerge:
404 405 # We've done a branch merge, mark this file as merged
405 406 # so that we properly record the merger later
406 407 repo.dirstate.merge(fd)
407 408 if f != f2: # copy/rename
408 409 if move:
409 410 repo.dirstate.remove(f)
410 411 if f != fd:
411 412 repo.dirstate.copy(f, fd)
412 413 else:
413 414 repo.dirstate.copy(f2, fd)
414 415 else:
415 416 # We've update-merged a locally modified file, so
416 417 # we set the dirstate to emulate a normal checkout
417 418 # of that file some time in the past. Thus our
418 419 # merge will appear as a normal local file
419 420 # modification.
420 421 if f2 == fd: # file not locally copied/moved
421 422 repo.dirstate.normallookup(fd)
422 423 if move:
423 424 repo.dirstate.forget(f)
424 425 elif m == "d": # directory rename
425 426 f2, fd, flag = a[2:]
426 427 if not f2 and f not in repo.dirstate:
427 428 # untracked file moved
428 429 continue
429 430 if branchmerge:
430 431 repo.dirstate.add(fd)
431 432 if f:
432 433 repo.dirstate.remove(f)
433 434 repo.dirstate.copy(f, fd)
434 435 if f2:
435 436 repo.dirstate.copy(f2, fd)
436 437 else:
437 438 repo.dirstate.normal(fd)
438 439 if f:
439 440 repo.dirstate.forget(f)
440 441
441 442 def update(repo, node, branchmerge, force, partial):
442 443 """
443 444 Perform a merge between the working directory and the given node
444 445
445 446 node = the node to update to, or None if unspecified
446 447 branchmerge = whether to merge between branches
447 448 force = whether to force branch merging or file overwriting
448 449 partial = a function to filter file lists (dirstate not updated)
449 450
450 451 The table below shows all the behaviors of the update command
451 452 given the -c and -C or no options, whether the working directory
452 453 is dirty, whether a revision is specified, and the relationship of
453 454 the parent rev to the target rev (linear, on the same named
454 455 branch, or on another named branch).
455 456
456 457 This logic is tested by test-update-branches.t.
457 458
458 459 -c -C dirty rev | linear same cross
459 460 n n n n | ok (1) x
460 461 n n n y | ok ok ok
461 462 n n y * | merge (2) (2)
462 463 n y * * | --- discard ---
463 464 y n y * | --- (3) ---
464 465 y n n * | --- ok ---
465 466 y y * * | --- (4) ---
466 467
467 468 x = can't happen
468 469 * = don't-care
469 470 1 = abort: crosses branches (use 'hg merge' or 'hg update -c')
470 471 2 = abort: crosses branches (use 'hg merge' to merge or
471 472 use 'hg update -C' to discard changes)
472 473 3 = abort: uncommitted local changes
473 474 4 = incompatible options (checked in commands.py)
474 475
475 476 Return the same tuple as applyupdates().
476 477 """
477 478
478 479 onode = node
479 480 wlock = repo.wlock()
480 481 try:
481 482 wc = repo[None]
482 483 if node is None:
483 484 # tip of current branch
484 485 try:
485 486 node = repo.branchtags()[wc.branch()]
486 487 except KeyError:
487 488 if wc.branch() == "default": # no default branch!
488 489 node = repo.lookup("tip") # update to tip
489 490 else:
490 491 raise util.Abort(_("branch %s not found") % wc.branch())
491 492 overwrite = force and not branchmerge
492 493 pl = wc.parents()
493 494 p1, p2 = pl[0], repo[node]
494 495 pa = p1.ancestor(p2)
495 496 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
496 497 fastforward = False
497 498
498 499 ### check phase
499 500 if not overwrite and len(pl) > 1:
500 501 raise util.Abort(_("outstanding uncommitted merges"))
501 502 if branchmerge:
502 503 if pa == p2:
503 504 raise util.Abort(_("merging with a working directory ancestor"
504 505 " has no effect"))
505 506 elif pa == p1:
506 507 if p1.branch() != p2.branch():
507 508 fastforward = True
508 509 else:
509 510 raise util.Abort(_("nothing to merge (use 'hg update'"
510 511 " or check 'hg heads')"))
511 512 if not force and (wc.files() or wc.deleted()):
512 513 raise util.Abort(_("outstanding uncommitted changes "
513 514 "(use 'hg status' to list changes)"))
514 515 for s in wc.substate:
515 516 if wc.sub(s).dirty():
516 517 raise util.Abort(_("outstanding uncommitted changes in "
517 518 "subrepository '%s'") % s)
518 519
519 520 elif not overwrite:
520 521 if pa == p1 or pa == p2: # linear
521 522 pass # all good
522 523 elif wc.files() or wc.deleted():
523 524 raise util.Abort(_("crosses branches (merge branches or use"
524 525 " --clean to discard changes)"))
525 526 elif onode is None:
526 527 raise util.Abort(_("crosses branches (merge branches or use"
527 528 " --check to force update)"))
528 529 else:
529 530 # Allow jumping branches if clean and specific rev given
530 531 overwrite = True
531 532
532 533 ### calculate phase
533 534 action = []
534 535 wc.status(unknown=True) # prime cache
535 536 if not force:
536 537 _checkunknown(wc, p2)
537 538 if not util.checkcase(repo.path):
538 539 _checkcollision(p2)
539 540 action += _forgetremoved(wc, p2, branchmerge)
540 541 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
541 542
542 543 ### apply phase
543 544 if not branchmerge: # just jump to the new rev
544 545 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
545 546 if not partial:
546 547 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
547 548
548 549 stats = applyupdates(repo, action, wc, p2, pa, overwrite)
549 550
550 551 if not partial:
551 552 repo.dirstate.setparents(fp1, fp2)
552 553 recordupdates(repo, action, branchmerge)
553 554 if not branchmerge and not fastforward:
554 555 repo.dirstate.setbranch(p2.branch())
555 556 finally:
556 557 wlock.release()
557 558
558 559 if not partial:
559 560 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
560 561 return stats
General Comments 0
You need to be logged in to leave comments. Login now