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