##// END OF EJS Templates
merge: simplify 'other deleted' case
Matt Mackall -
r8744:6b675c78 default
parent child Browse files
Show More
@@ -1,499 +1,499 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, incorporated herein by reference.
7 7
8 8 from node import nullid, nullrev, hex, bin
9 9 from i18n import _
10 10 import util, filemerge, copies
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 repo.ui.note(_("resolving manifests\n"))
129 129 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
130 130 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
131 131
132 132 m1 = p1.manifest()
133 133 m2 = p2.manifest()
134 134 backwards = (pa == p2)
135 135
136 136 if overwrite:
137 137 ma = m1
138 138 elif backwards:
139 139 ma = p1.p1().manifest()
140 140 else:
141 141 ma = pa.manifest()
142 142
143 143 action = []
144 144 copy, copied, diverge = {}, {}, {}
145 145
146 146 def fmerge(f, f2, fa):
147 147 """merge flags"""
148 148 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
149 149 if m == n: # flags agree
150 150 return m # unchanged
151 151 if m and n and not a: # flags set, don't agree, differ from parent
152 152 r = repo.ui.prompt(
153 153 _(" conflicting flags for %s\n"
154 154 "(n)one, e(x)ec or sym(l)ink?") % f,
155 155 (_("&None"), _("E&xec"), _("Sym&link")), _("n"))
156 156 return r != _("n") and r or ''
157 157 if m and m != a: # changed from a to m
158 158 return m
159 159 if n and n != a: # changed from a to n
160 160 return n
161 161 return '' # flag was cleared
162 162
163 163 def act(msg, m, f, *args):
164 164 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
165 165 action.append((f, m) + args)
166 166
167 167 if pa and not (backwards or overwrite):
168 168 if 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 copied = set(copy.values())
172 172 for of, fl in diverge.iteritems():
173 173 act("divergent renames", "dr", of, fl)
174 174
175 175 # Compare manifests
176 176 for f, n in m1.iteritems():
177 177 if partial and not partial(f):
178 178 continue
179 179 if f in m2:
180 180 rflags = fmerge(f, f, f)
181 181 # are files different?
182 182 if n != m2[f]:
183 183 a = ma.get(f, nullid)
184 184 # are both different from the ancestor?
185 185 if n != a and m2[f] != a:
186 186 act("versions differ", "m", f, f, f, rflags, False)
187 187 # is remote's version newer?
188 188 elif m2[f] != a:
189 189 # are we clobbering?
190 190 if overwrite:
191 191 act("clobbering", "g", f, rflags)
192 192 # or are we going back in time and clean?
193 193 elif backwards:
194 194 act("reverting", "g", f, rflags)
195 195 else:
196 196 act("remote is newer", "g", f, rflags)
197 197 # local is newer, not overwrite, check mode bits
198 198 elif m1.flags(f) != rflags:
199 199 act("update permissions", "e", f, rflags)
200 200 # contents same, check mode bits
201 201 elif m1.flags(f) != rflags:
202 202 act("update permissions", "e", f, rflags)
203 203 elif f in copied:
204 204 continue
205 205 elif f in copy:
206 206 f2 = copy[f]
207 207 if f2 not in m2: # directory rename
208 208 act("remote renamed directory to " + f2, "d",
209 209 f, None, f2, m1.flags(f))
210 210 elif f2 in m1: # case 2 A,B/B/B
211 211 act("local copied to " + f2, "m",
212 212 f, f2, f, fmerge(f, f2, f2), False)
213 213 else: # case 4,21 A/B/B
214 214 act("local moved to " + f2, "m",
215 215 f, f2, f, fmerge(f, f2, f2), False)
216 elif f in ma and not n[20:]:
216 elif n[20:] == "a": # added, no remote
217 act("remote deleted", "f", f)
218 elif f in ma: # clean, a different, no remote
217 219 if n != ma[f]:
218 220 if repo.ui.prompt(
219 221 _(" local changed %s which remote deleted\n"
220 222 "use (c)hanged version or (d)elete?") % f,
221 223 (_("&Changed"), _("&Delete")), _("c")) == _("d"):
222 224 act("prompt delete", "r", f)
223 225 else:
224 226 act("prompt keep", "a", f)
225 else:
227 elif n[20:] != "u":
226 228 act("other deleted", "r", f)
227 elif n[20:] == "a": # only forget locally-added
228 act("remote deleted", "f", f)
229 229 else:
230 230 # file is created on branch or in working directory
231 231 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
232 232 act("remote deleted", "r", f)
233 233
234 234 for f, n in m2.iteritems():
235 235 if partial and not partial(f):
236 236 continue
237 237 if f in m1:
238 238 continue
239 239 if f in copied:
240 240 continue
241 241 if f in copy:
242 242 f2 = copy[f]
243 243 if f2 not in m1: # directory rename
244 244 act("local renamed directory to " + f2, "d",
245 245 None, f, f2, m2.flags(f))
246 246 elif f2 in m2: # rename case 1, A/A,B/A
247 247 act("remote copied to " + f, "m",
248 248 f2, f, f, fmerge(f2, f, f2), False)
249 249 else: # case 3,20 A/B/A
250 250 act("remote moved to " + f, "m",
251 251 f2, f, f, fmerge(f2, f, f2), True)
252 252 elif f not in ma:
253 253 act("remote created", "g", f, m2.flags(f))
254 254 elif n != ma[f]:
255 255 if repo.ui.prompt(
256 256 _("remote changed %s which local deleted\n"
257 257 "use (c)hanged version or leave (d)eleted?") % f,
258 258 (_("&Changed"), _("&Deleted")), _("c")) == _("c"):
259 259 act("prompt recreating", "g", f, m2.flags(f))
260 260
261 261 return action
262 262
263 263 def actionkey(a):
264 264 return a[1] == 'r' and -1 or 0, a
265 265
266 266 def applyupdates(repo, action, wctx, mctx):
267 267 "apply the merge action list to the working directory"
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
275 275 # prescan for merges
276 276 for a in action:
277 277 f, m = a[:2]
278 278 if m == 'm': # merge
279 279 f2, fd, flags, move = a[2:]
280 280 repo.ui.debug(_("preserving %s for resolve of %s\n") % (f, fd))
281 281 fcl = wctx[f]
282 282 fco = mctx[f2]
283 283 fca = fcl.ancestor(fco) or repo.filectx(f, fileid=nullrev)
284 284 ms.add(fcl, fco, fca, fd, flags)
285 285 if f != fd and move:
286 286 moves.append(f)
287 287
288 288 # remove renamed files after safely stored
289 289 for f in moves:
290 290 if util.lexists(repo.wjoin(f)):
291 291 repo.ui.debug(_("removing %s\n") % f)
292 292 os.unlink(repo.wjoin(f))
293 293
294 294 audit_path = util.path_auditor(repo.root)
295 295
296 296 for a in action:
297 297 f, m = a[:2]
298 298 if f and f[0] == "/":
299 299 continue
300 300 if m == "r": # remove
301 301 repo.ui.note(_("removing %s\n") % f)
302 302 audit_path(f)
303 303 try:
304 304 util.unlink(repo.wjoin(f))
305 305 except OSError, inst:
306 306 if inst.errno != errno.ENOENT:
307 307 repo.ui.warn(_("update failed to remove %s: %s!\n") %
308 308 (f, inst.strerror))
309 309 removed += 1
310 310 elif m == "m": # merge
311 311 f2, fd, flags, move = a[2:]
312 312 r = ms.resolve(fd, wctx, mctx)
313 313 if r > 0:
314 314 unresolved += 1
315 315 else:
316 316 if r is None:
317 317 updated += 1
318 318 else:
319 319 merged += 1
320 320 util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
321 321 if f != fd and move and util.lexists(repo.wjoin(f)):
322 322 repo.ui.debug(_("removing %s\n") % f)
323 323 os.unlink(repo.wjoin(f))
324 324 elif m == "g": # get
325 325 flags = a[2]
326 326 repo.ui.note(_("getting %s\n") % f)
327 327 t = mctx.filectx(f).data()
328 328 repo.wwrite(f, t, flags)
329 329 updated += 1
330 330 elif m == "d": # directory rename
331 331 f2, fd, flags = a[2:]
332 332 if f:
333 333 repo.ui.note(_("moving %s to %s\n") % (f, fd))
334 334 t = wctx.filectx(f).data()
335 335 repo.wwrite(fd, t, flags)
336 336 util.unlink(repo.wjoin(f))
337 337 if f2:
338 338 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
339 339 t = mctx.filectx(f2).data()
340 340 repo.wwrite(fd, t, flags)
341 341 updated += 1
342 342 elif m == "dr": # divergent renames
343 343 fl = a[2]
344 344 repo.ui.warn(_("warning: detected divergent renames of %s to:\n") % f)
345 345 for nf in fl:
346 346 repo.ui.warn(" %s\n" % nf)
347 347 elif m == "e": # exec
348 348 flags = a[2]
349 349 util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags)
350 350
351 351 return updated, merged, removed, unresolved
352 352
353 353 def recordupdates(repo, action, branchmerge):
354 354 "record merge actions to the dirstate"
355 355
356 356 for a in action:
357 357 f, m = a[:2]
358 358 if m == "r": # remove
359 359 if branchmerge:
360 360 repo.dirstate.remove(f)
361 361 else:
362 362 repo.dirstate.forget(f)
363 363 elif m == "a": # re-add
364 364 if not branchmerge:
365 365 repo.dirstate.add(f)
366 366 elif m == "f": # forget
367 367 repo.dirstate.forget(f)
368 368 elif m == "e": # exec change
369 369 repo.dirstate.normallookup(f)
370 370 elif m == "g": # get
371 371 if branchmerge:
372 372 repo.dirstate.normaldirty(f)
373 373 else:
374 374 repo.dirstate.normal(f)
375 375 elif m == "m": # merge
376 376 f2, fd, flag, move = a[2:]
377 377 if branchmerge:
378 378 # We've done a branch merge, mark this file as merged
379 379 # so that we properly record the merger later
380 380 repo.dirstate.merge(fd)
381 381 if f != f2: # copy/rename
382 382 if move:
383 383 repo.dirstate.remove(f)
384 384 if f != fd:
385 385 repo.dirstate.copy(f, fd)
386 386 else:
387 387 repo.dirstate.copy(f2, fd)
388 388 else:
389 389 # We've update-merged a locally modified file, so
390 390 # we set the dirstate to emulate a normal checkout
391 391 # of that file some time in the past. Thus our
392 392 # merge will appear as a normal local file
393 393 # modification.
394 394 repo.dirstate.normallookup(fd)
395 395 if move:
396 396 repo.dirstate.forget(f)
397 397 elif m == "d": # directory rename
398 398 f2, fd, flag = a[2:]
399 399 if not f2 and f not in repo.dirstate:
400 400 # untracked file moved
401 401 continue
402 402 if branchmerge:
403 403 repo.dirstate.add(fd)
404 404 if f:
405 405 repo.dirstate.remove(f)
406 406 repo.dirstate.copy(f, fd)
407 407 if f2:
408 408 repo.dirstate.copy(f2, fd)
409 409 else:
410 410 repo.dirstate.normal(fd)
411 411 if f:
412 412 repo.dirstate.forget(f)
413 413
414 414 def update(repo, node, branchmerge, force, partial):
415 415 """
416 416 Perform a merge between the working directory and the given node
417 417
418 418 branchmerge = whether to merge between branches
419 419 force = whether to force branch merging or file overwriting
420 420 partial = a function to filter file lists (dirstate not updated)
421 421 """
422 422
423 423 wlock = repo.wlock()
424 424 try:
425 425 wc = repo[None]
426 426 if node is None:
427 427 # tip of current branch
428 428 try:
429 429 node = repo.branchtags()[wc.branch()]
430 430 except KeyError:
431 431 if wc.branch() == "default": # no default branch!
432 432 node = repo.lookup("tip") # update to tip
433 433 else:
434 434 raise util.Abort(_("branch %s not found") % wc.branch())
435 435 overwrite = force and not branchmerge
436 436 pl = wc.parents()
437 437 p1, p2 = pl[0], repo[node]
438 438 pa = p1.ancestor(p2)
439 439 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
440 440 fastforward = False
441 441
442 442 ### check phase
443 443 if not overwrite and len(pl) > 1:
444 444 raise util.Abort(_("outstanding uncommitted merges"))
445 445 if branchmerge:
446 446 if pa == p2:
447 447 raise util.Abort(_("can't merge with ancestor"))
448 448 elif pa == p1:
449 449 if p1.branch() != p2.branch():
450 450 fastforward = True
451 451 else:
452 452 raise util.Abort(_("nothing to merge (use 'hg update'"
453 453 " or check 'hg heads')"))
454 454 if not force and (wc.files() or wc.deleted()):
455 455 raise util.Abort(_("outstanding uncommitted changes "
456 456 "(use 'hg status' to list changes)"))
457 457 elif not overwrite:
458 458 if pa == p1 or pa == p2: # linear
459 459 pass # all good
460 460 elif p1.branch() == p2.branch():
461 461 if wc.files() or wc.deleted():
462 462 raise util.Abort(_("crosses branches (use 'hg merge' or "
463 463 "'hg update -C' to discard changes)"))
464 464 raise util.Abort(_("crosses branches (use 'hg merge' "
465 465 "or 'hg update -C')"))
466 466 elif wc.files() or wc.deleted():
467 467 raise util.Abort(_("crosses named branches (use "
468 468 "'hg update -C' to discard changes)"))
469 469 else:
470 470 # Allow jumping branches if there are no changes
471 471 overwrite = True
472 472
473 473 ### calculate phase
474 474 action = []
475 475 if not force:
476 476 _checkunknown(wc, p2)
477 477 if not util.checkcase(repo.path):
478 478 _checkcollision(p2)
479 479 action += _forgetremoved(wc, p2, branchmerge)
480 480 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
481 481
482 482 ### apply phase
483 483 if not branchmerge: # just jump to the new rev
484 484 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
485 485 if not partial:
486 486 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
487 487
488 488 stats = applyupdates(repo, action, wc, p2)
489 489
490 490 if not partial:
491 491 recordupdates(repo, action, branchmerge)
492 492 repo.dirstate.setparents(fp1, fp2)
493 493 if not branchmerge and not fastforward:
494 494 repo.dirstate.setbranch(p2.branch())
495 495 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
496 496
497 497 return stats
498 498 finally:
499 499 wlock.release()
General Comments 0
You need to be logged in to leave comments. Login now