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