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