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