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