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