##// END OF EJS Templates
merge: handle subrepo merges and .hgsubstate specially...
Bryan O'Sullivan -
r18632:3e200791 default
parent child Browse files
Show More
@@ -1,688 +1,690 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 error, 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):
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(), fcl.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 fcd = wctx[dfile]
71 71 fco = octx[ofile]
72 72 fca = self._repo.filectx(afile, fileid=anode)
73 73 # "premerge" x flags
74 74 flo = fco.flags()
75 75 fla = fca.flags()
76 76 if 'x' in flags + flo + fla and 'l' not in flags + flo + fla:
77 77 if fca.node() == nullid:
78 78 self._repo.ui.warn(_('warning: cannot merge flags for %s\n') %
79 79 afile)
80 80 elif flags == fla:
81 81 flags = flo
82 82 # restore local
83 83 f = self._repo.opener("merge/" + hash)
84 84 self._repo.wwrite(dfile, f.read(), flags)
85 85 f.close()
86 86 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
87 87 if r is None:
88 88 # no real conflict
89 89 del self._state[dfile]
90 90 elif not r:
91 91 self.mark(dfile, 'r')
92 92 return r
93 93
94 94 def _checkunknownfile(repo, wctx, mctx, f):
95 95 return (not repo.dirstate._ignore(f)
96 96 and os.path.isfile(repo.wjoin(f))
97 97 and repo.dirstate.normalize(f) not in repo.dirstate
98 98 and mctx[f].cmp(wctx[f]))
99 99
100 100 def _checkunknown(repo, wctx, mctx):
101 101 "check for collisions between unknown files and files in mctx"
102 102
103 103 error = False
104 104 for f in mctx:
105 105 if f not in wctx and _checkunknownfile(repo, wctx, mctx, f):
106 106 error = True
107 107 wctx._repo.ui.warn(_("%s: untracked file differs\n") % f)
108 108 if error:
109 109 raise util.Abort(_("untracked files in working directory differ "
110 110 "from files in requested revision"))
111 111
112 112 def _remains(f, m, ma, workingctx=False):
113 113 """check whether specified file remains after merge.
114 114
115 115 It is assumed that specified file is not contained in the manifest
116 116 of the other context.
117 117 """
118 118 if f in ma:
119 119 n = m[f]
120 120 if n != ma[f]:
121 121 return True # because it is changed locally
122 122 # even though it doesn't remain, if "remote deleted" is
123 123 # chosen in manifestmerge()
124 124 elif workingctx and n[20:] == "a":
125 125 return True # because it is added locally (linear merge specific)
126 126 else:
127 127 return False # because it is removed remotely
128 128 else:
129 129 return True # because it is added locally
130 130
131 131 def _checkcollision(mctx, extractxs):
132 132 "check for case folding collisions in the destination context"
133 133 folded = {}
134 134 for fn in mctx:
135 135 fold = util.normcase(fn)
136 136 if fold in folded:
137 137 raise util.Abort(_("case-folding collision between %s and %s")
138 138 % (fn, folded[fold]))
139 139 folded[fold] = fn
140 140
141 141 if extractxs:
142 142 wctx, actx = extractxs
143 143 # class to delay looking up copy mapping
144 144 class pathcopies(object):
145 145 @util.propertycache
146 146 def map(self):
147 147 # {dst@mctx: src@wctx} copy mapping
148 148 return copies.pathcopies(wctx, mctx)
149 149 pc = pathcopies()
150 150
151 151 for fn in wctx:
152 152 fold = util.normcase(fn)
153 153 mfn = folded.get(fold, None)
154 154 if (mfn and mfn != fn and pc.map.get(mfn) != fn and
155 155 _remains(fn, wctx.manifest(), actx.manifest(), True) and
156 156 _remains(mfn, mctx.manifest(), actx.manifest())):
157 157 raise util.Abort(_("case-folding collision between %s and %s")
158 158 % (mfn, fn))
159 159
160 160 def _forgetremoved(wctx, mctx, branchmerge):
161 161 """
162 162 Forget removed files
163 163
164 164 If we're jumping between revisions (as opposed to merging), and if
165 165 neither the working directory nor the target rev has the file,
166 166 then we need to remove it from the dirstate, to prevent the
167 167 dirstate from listing the file when it is no longer in the
168 168 manifest.
169 169
170 170 If we're merging, and the other revision has removed a file
171 171 that is not present in the working directory, we need to mark it
172 172 as removed.
173 173 """
174 174
175 175 actions = []
176 176 state = branchmerge and 'r' or 'f'
177 177 for f in wctx.deleted():
178 178 if f not in mctx:
179 179 actions.append((f, state, None, "forget deleted"))
180 180
181 181 if not branchmerge:
182 182 for f in wctx.removed():
183 183 if f not in mctx:
184 184 actions.append((f, "f", None, "forget removed"))
185 185
186 186 return actions
187 187
188 188 def manifestmerge(repo, wctx, p2, pa, branchmerge, force, partial):
189 189 """
190 190 Merge p1 and p2 with ancestor pa and generate merge action list
191 191
192 192 branchmerge and force are as passed in to update
193 193 partial = function to filter file lists
194 194 """
195 195
196 196 overwrite = force and not branchmerge
197 197 actions, copy, movewithdir = [], {}, {}
198 198
199 199 if overwrite:
200 200 pa = wctx
201 201 elif pa == p2: # backwards
202 202 pa = wctx.p1()
203 203 elif not branchmerge and not wctx.dirty(missing=True):
204 204 pass
205 205 elif pa and repo.ui.configbool("merge", "followcopies", True):
206 206 ret = copies.mergecopies(repo, wctx, p2, pa)
207 207 copy, movewithdir, diverge, renamedelete = ret
208 208 for of, fl in diverge.iteritems():
209 209 actions.append((of, "dr", (fl,), "divergent renames"))
210 210 for of, fl in renamedelete.iteritems():
211 211 actions.append((of, "rd", (fl,), "rename and delete"))
212 212
213 213 repo.ui.note(_("resolving manifests\n"))
214 214 repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n"
215 215 % (bool(branchmerge), bool(force), bool(partial)))
216 216 repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
217 217
218 218 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
219 219 copied = set(copy.values())
220 220 copied.update(movewithdir.values())
221 221
222 222 if '.hgsubstate' in m1:
223 223 # check whether sub state is modified
224 224 for s in sorted(wctx.substate):
225 225 if wctx.sub(s).dirty():
226 226 m1['.hgsubstate'] += "+"
227 227 break
228 228
229 229 aborts, prompts = [], []
230 230 # Compare manifests
231 231 for f, n in m1.iteritems():
232 232 if partial and not partial(f):
233 233 continue
234 234 if f in m2:
235 235 n2 = m2[f]
236 236 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
237 237 nol = 'l' not in fl1 + fl2 + fla
238 238 a = ma.get(f, nullid)
239 239 if n == n2 and fl1 == fl2:
240 240 pass # same - keep local
241 241 elif n2 == a and fl2 == fla:
242 242 pass # remote unchanged - keep local
243 243 elif n == a and fl1 == fla: # local unchanged - use remote
244 244 if n == n2: # optimization: keep local content
245 245 actions.append((f, "e", (fl2,), "update permissions"))
246 246 else:
247 247 actions.append((f, "g", (fl2,), "remote is newer"))
248 248 elif nol and n2 == a: # remote only changed 'x'
249 249 actions.append((f, "e", (fl2,), "update permissions"))
250 250 elif nol and n == a: # local only changed 'x'
251 251 actions.append((f, "g", (fl1,), "remote is newer"))
252 252 else: # both changed something
253 253 actions.append((f, "m", (f, f, False), "versions differ"))
254 254 elif f in copied: # files we'll deal with on m2 side
255 255 pass
256 256 elif f in movewithdir: # directory rename
257 257 f2 = movewithdir[f]
258 258 actions.append((f, "d", (None, f2, m1.flags(f)),
259 259 "remote renamed directory to " + f2))
260 260 elif f in copy:
261 261 f2 = copy[f]
262 262 actions.append((f, "m", (f2, f, False),
263 263 "local copied/moved to " + f2))
264 264 elif f in ma: # clean, a different, no remote
265 265 if n != ma[f]:
266 266 prompts.append((f, "cd")) # prompt changed/deleted
267 267 elif n[20:] == "a": # added, no remote
268 268 actions.append((f, "f", None, "remote deleted"))
269 269 else:
270 270 actions.append((f, "r", None, "other deleted"))
271 271
272 272 for f, n in m2.iteritems():
273 273 if partial and not partial(f):
274 274 continue
275 275 if f in m1 or f in copied: # files already visited
276 276 continue
277 277 if f in movewithdir:
278 278 f2 = movewithdir[f]
279 279 actions.append((None, "d", (f, f2, m2.flags(f)),
280 280 "local renamed directory to " + f2))
281 281 elif f in copy:
282 282 f2 = copy[f]
283 283 if f2 in m2:
284 284 actions.append((f2, "m", (f, f, False),
285 285 "remote copied to " + f))
286 286 else:
287 287 actions.append((f2, "m", (f, f, True),
288 288 "remote moved to " + f))
289 289 elif f not in ma:
290 290 # local unknown, remote created: the logic is described by the
291 291 # following table:
292 292 #
293 293 # force branchmerge different | action
294 294 # n * n | get
295 295 # n * y | abort
296 296 # y n * | get
297 297 # y y n | get
298 298 # y y y | merge
299 299 #
300 300 # Checking whether the files are different is expensive, so we
301 301 # don't do that when we can avoid it.
302 302 if force and not branchmerge:
303 303 actions.append((f, "g", (m2.flags(f),), "remote created"))
304 304 else:
305 305 different = _checkunknownfile(repo, wctx, p2, f)
306 306 if force and branchmerge and different:
307 307 actions.append((f, "m", (f, f, False),
308 308 "remote differs from untracked local"))
309 309 elif not force and different:
310 310 aborts.append((f, "ud"))
311 311 else:
312 312 actions.append((f, "g", (m2.flags(f),), "remote created"))
313 313 elif n != ma[f]:
314 314 prompts.append((f, "dc")) # prompt deleted/changed
315 315
316 316 for f, m in sorted(aborts):
317 317 if m == "ud":
318 318 repo.ui.warn(_("%s: untracked file differs\n") % f)
319 319 else: assert False, m
320 320 if aborts:
321 321 raise util.Abort(_("untracked files in working directory differ "
322 322 "from files in requested revision"))
323 323
324 324 for f, m in sorted(prompts):
325 325 if m == "cd":
326 326 if repo.ui.promptchoice(
327 327 _("local changed %s which remote deleted\n"
328 328 "use (c)hanged version or (d)elete?") % f,
329 329 (_("&Changed"), _("&Delete")), 0):
330 330 actions.append((f, "r", None, "prompt delete"))
331 331 else:
332 332 actions.append((f, "a", None, "prompt keep"))
333 333 elif m == "dc":
334 334 if repo.ui.promptchoice(
335 335 _("remote changed %s which local deleted\n"
336 336 "use (c)hanged version or leave (d)eleted?") % f,
337 337 (_("&Changed"), _("&Deleted")), 0) == 0:
338 338 actions.append((f, "g", (m2.flags(f),), "prompt recreating"))
339 339 else: assert False, m
340 340 return actions
341 341
342 342 def actionkey(a):
343 343 return a[1] == "r" and -1 or 0, a
344 344
345 def getremove(repo, wctx, mctx, overwrite, args):
345 def getremove(repo, mctx, overwrite, args):
346 346 """apply usually-non-interactive updates to the working directory
347 347
348 wctx is the working copy context
349 348 mctx is the context to be merged into the working copy
350 349
351 350 yields tuples for progress updates
352 351 """
353 352 audit = repo.wopener.audit
354 353 for i, arg in enumerate(args):
355 354 f = arg[0]
356 355 if arg[1] == 'r':
357 356 repo.ui.note(_("removing %s\n") % f)
358 if f == '.hgsubstate': # subrepo states need updating
359 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
360 357 audit(f)
361 358 try:
362 359 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
363 360 except OSError, inst:
364 361 repo.ui.warn(_("update failed to remove %s: %s!\n") %
365 362 (f, inst.strerror))
366 363 else:
367 364 repo.ui.note(_("getting %s\n") % f)
368 365 repo.wwrite(f, mctx.filectx(f).data(), arg[2][0])
369 if f == '.hgsubstate': # subrepo states need updating
370 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
371 366 yield i, f
372 367
373 368 def applyupdates(repo, actions, wctx, mctx, actx, overwrite):
374 369 """apply the merge action list to the working directory
375 370
376 371 wctx is the working copy context
377 372 mctx is the context to be merged into the working copy
378 373 actx is the context of the common ancestor
379 374
380 375 Return a tuple of counts (updated, merged, removed, unresolved) that
381 376 describes how many files were affected by the update.
382 377 """
383 378
384 379 updated, merged, removed, unresolved = 0, 0, 0, 0
385 380 ms = mergestate(repo)
386 381 ms.reset(wctx.p1().node())
387 382 moves = []
388 383 actions.sort(key=actionkey)
389 384
390 385 # prescan for merges
391 386 for a in actions:
392 387 f, m, args, msg = a
393 388 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
394 389 if m == "m": # merge
395 390 f2, fd, move = args
396 391 if fd == '.hgsubstate': # merged internally
397 392 continue
398 393 repo.ui.debug(" preserving %s for resolve of %s\n" % (f, fd))
399 394 fcl = wctx[f]
400 395 fco = mctx[f2]
401 396 if mctx == actx: # backwards, use working dir parent as ancestor
402 397 if fcl.parents():
403 398 fca = fcl.p1()
404 399 else:
405 400 fca = repo.filectx(f, fileid=nullrev)
406 401 else:
407 402 fca = fcl.ancestor(fco, actx)
408 403 if not fca:
409 404 fca = repo.filectx(f, fileid=nullrev)
410 405 ms.add(fcl, fco, fca, fd)
411 406 if f != fd and move:
412 407 moves.append(f)
413 408
414 409 audit = repo.wopener.audit
415 410
416 411 # remove renamed files after safely stored
417 412 for f in moves:
418 413 if os.path.lexists(repo.wjoin(f)):
419 414 repo.ui.debug("removing %s\n" % f)
420 415 audit(f)
421 416 util.unlinkpath(repo.wjoin(f))
422 417
423 418 numupdates = len(actions)
424 419 workeractions = [a for a in actions if a[1] in 'gr']
425 420 updated = len([a for a in workeractions if a[1] == 'g'])
426 421 removed = len([a for a in workeractions if a[1] == 'r'])
427 422 actions = [a for a in actions if a[1] not in 'gr']
428 423
429 for i, item in getremove(repo, wctx, mctx, overwrite, workeractions):
424 hgsub = [a[1] for a in workeractions if a[0] == '.hgsubstate']
425 if hgsub and hgsub[0] == 'r':
426 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
427
428 for i, item in getremove(repo, mctx, overwrite, workeractions):
430 429 repo.ui.progress(_('updating'), i + 1, item=item, total=numupdates,
431 430 unit=_('files'))
432 431
432 if hgsub and hgsub[0] == 'g':
433 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
434
433 435 z = len(workeractions)
434 436
435 437 for i, a in enumerate(actions):
436 438 f, m, args, msg = a
437 439 repo.ui.progress(_('updating'), z + i + 1, item=f, total=numupdates,
438 440 unit=_('files'))
439 441 if m == "m": # merge
440 442 if fd == '.hgsubstate': # subrepo states need updating
441 443 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
442 444 overwrite)
443 445 continue
444 446 f2, fd, move = args
445 447 audit(fd)
446 448 r = ms.resolve(fd, wctx, mctx)
447 449 if r is not None and r > 0:
448 450 unresolved += 1
449 451 else:
450 452 if r is None:
451 453 updated += 1
452 454 else:
453 455 merged += 1
454 456 elif m == "d": # directory rename
455 457 f2, fd, flags = args
456 458 if f:
457 459 repo.ui.note(_("moving %s to %s\n") % (f, fd))
458 460 audit(f)
459 461 repo.wwrite(fd, wctx.filectx(f).data(), flags)
460 462 util.unlinkpath(repo.wjoin(f))
461 463 if f2:
462 464 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
463 465 repo.wwrite(fd, mctx.filectx(f2).data(), flags)
464 466 updated += 1
465 467 elif m == "dr": # divergent renames
466 468 fl, = args
467 469 repo.ui.warn(_("note: possible conflict - %s was renamed "
468 470 "multiple times to:\n") % f)
469 471 for nf in fl:
470 472 repo.ui.warn(" %s\n" % nf)
471 473 elif m == "rd": # rename and delete
472 474 fl, = args
473 475 repo.ui.warn(_("note: possible conflict - %s was deleted "
474 476 "and renamed to:\n") % f)
475 477 for nf in fl:
476 478 repo.ui.warn(" %s\n" % nf)
477 479 elif m == "e": # exec
478 480 flags, = args
479 481 audit(f)
480 482 util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
481 483 updated += 1
482 484 ms.commit()
483 485 repo.ui.progress(_('updating'), None, total=numupdates, unit=_('files'))
484 486
485 487 return updated, merged, removed, unresolved
486 488
487 489 def calculateupdates(repo, tctx, mctx, ancestor, branchmerge, force, partial):
488 490 "Calculate the actions needed to merge mctx into tctx"
489 491 actions = []
490 492 folding = not util.checkcase(repo.path)
491 493 if folding:
492 494 # collision check is not needed for clean update
493 495 if (not branchmerge and
494 496 (force or not tctx.dirty(missing=True, branch=False))):
495 497 _checkcollision(mctx, None)
496 498 else:
497 499 _checkcollision(mctx, (tctx, ancestor))
498 500 if tctx.rev() is None:
499 501 actions += _forgetremoved(tctx, mctx, branchmerge)
500 502 actions += manifestmerge(repo, tctx, mctx,
501 503 ancestor,
502 504 branchmerge, force,
503 505 partial)
504 506 return actions
505 507
506 508 def recordupdates(repo, actions, branchmerge):
507 509 "record merge actions to the dirstate"
508 510
509 511 for a in actions:
510 512 f, m, args, msg = a
511 513 if m == "r": # remove
512 514 if branchmerge:
513 515 repo.dirstate.remove(f)
514 516 else:
515 517 repo.dirstate.drop(f)
516 518 elif m == "a": # re-add
517 519 if not branchmerge:
518 520 repo.dirstate.add(f)
519 521 elif m == "f": # forget
520 522 repo.dirstate.drop(f)
521 523 elif m == "e": # exec change
522 524 repo.dirstate.normallookup(f)
523 525 elif m == "g": # get
524 526 if branchmerge:
525 527 repo.dirstate.otherparent(f)
526 528 else:
527 529 repo.dirstate.normal(f)
528 530 elif m == "m": # merge
529 531 f2, fd, move = args
530 532 if branchmerge:
531 533 # We've done a branch merge, mark this file as merged
532 534 # so that we properly record the merger later
533 535 repo.dirstate.merge(fd)
534 536 if f != f2: # copy/rename
535 537 if move:
536 538 repo.dirstate.remove(f)
537 539 if f != fd:
538 540 repo.dirstate.copy(f, fd)
539 541 else:
540 542 repo.dirstate.copy(f2, fd)
541 543 else:
542 544 # We've update-merged a locally modified file, so
543 545 # we set the dirstate to emulate a normal checkout
544 546 # of that file some time in the past. Thus our
545 547 # merge will appear as a normal local file
546 548 # modification.
547 549 if f2 == fd: # file not locally copied/moved
548 550 repo.dirstate.normallookup(fd)
549 551 if move:
550 552 repo.dirstate.drop(f)
551 553 elif m == "d": # directory rename
552 554 f2, fd, flag = args
553 555 if not f2 and f not in repo.dirstate:
554 556 # untracked file moved
555 557 continue
556 558 if branchmerge:
557 559 repo.dirstate.add(fd)
558 560 if f:
559 561 repo.dirstate.remove(f)
560 562 repo.dirstate.copy(f, fd)
561 563 if f2:
562 564 repo.dirstate.copy(f2, fd)
563 565 else:
564 566 repo.dirstate.normal(fd)
565 567 if f:
566 568 repo.dirstate.drop(f)
567 569
568 570 def update(repo, node, branchmerge, force, partial, ancestor=None,
569 571 mergeancestor=False):
570 572 """
571 573 Perform a merge between the working directory and the given node
572 574
573 575 node = the node to update to, or None if unspecified
574 576 branchmerge = whether to merge between branches
575 577 force = whether to force branch merging or file overwriting
576 578 partial = a function to filter file lists (dirstate not updated)
577 579 mergeancestor = if false, merging with an ancestor (fast-forward)
578 580 is only allowed between different named branches. This flag
579 581 is used by rebase extension as a temporary fix and should be
580 582 avoided in general.
581 583
582 584 The table below shows all the behaviors of the update command
583 585 given the -c and -C or no options, whether the working directory
584 586 is dirty, whether a revision is specified, and the relationship of
585 587 the parent rev to the target rev (linear, on the same named
586 588 branch, or on another named branch).
587 589
588 590 This logic is tested by test-update-branches.t.
589 591
590 592 -c -C dirty rev | linear same cross
591 593 n n n n | ok (1) x
592 594 n n n y | ok ok ok
593 595 n n y * | merge (2) (2)
594 596 n y * * | --- discard ---
595 597 y n y * | --- (3) ---
596 598 y n n * | --- ok ---
597 599 y y * * | --- (4) ---
598 600
599 601 x = can't happen
600 602 * = don't-care
601 603 1 = abort: crosses branches (use 'hg merge' or 'hg update -c')
602 604 2 = abort: crosses branches (use 'hg merge' to merge or
603 605 use 'hg update -C' to discard changes)
604 606 3 = abort: uncommitted local changes
605 607 4 = incompatible options (checked in commands.py)
606 608
607 609 Return the same tuple as applyupdates().
608 610 """
609 611
610 612 onode = node
611 613 wlock = repo.wlock()
612 614 try:
613 615 wc = repo[None]
614 616 if node is None:
615 617 # tip of current branch
616 618 try:
617 619 node = repo.branchtip(wc.branch())
618 620 except error.RepoLookupError:
619 621 if wc.branch() == "default": # no default branch!
620 622 node = repo.lookup("tip") # update to tip
621 623 else:
622 624 raise util.Abort(_("branch %s not found") % wc.branch())
623 625 overwrite = force and not branchmerge
624 626 pl = wc.parents()
625 627 p1, p2 = pl[0], repo[node]
626 628 if ancestor:
627 629 pa = repo[ancestor]
628 630 else:
629 631 pa = p1.ancestor(p2)
630 632
631 633 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
632 634
633 635 ### check phase
634 636 if not overwrite and len(pl) > 1:
635 637 raise util.Abort(_("outstanding uncommitted merges"))
636 638 if branchmerge:
637 639 if pa == p2:
638 640 raise util.Abort(_("merging with a working directory ancestor"
639 641 " has no effect"))
640 642 elif pa == p1:
641 643 if not mergeancestor and p1.branch() == p2.branch():
642 644 raise util.Abort(_("nothing to merge"),
643 645 hint=_("use 'hg update' "
644 646 "or check 'hg heads'"))
645 647 if not force and (wc.files() or wc.deleted()):
646 648 raise util.Abort(_("outstanding uncommitted changes"),
647 649 hint=_("use 'hg status' to list changes"))
648 650 for s in sorted(wc.substate):
649 651 if wc.sub(s).dirty():
650 652 raise util.Abort(_("outstanding uncommitted changes in "
651 653 "subrepository '%s'") % s)
652 654
653 655 elif not overwrite:
654 656 if pa == p1 or pa == p2: # linear
655 657 pass # all good
656 658 elif wc.dirty(missing=True):
657 659 raise util.Abort(_("crosses branches (merge branches or use"
658 660 " --clean to discard changes)"))
659 661 elif onode is None:
660 662 raise util.Abort(_("crosses branches (merge branches or update"
661 663 " --check to force update)"))
662 664 else:
663 665 # Allow jumping branches if clean and specific rev given
664 666 pa = p1
665 667
666 668 ### calculate phase
667 669 actions = calculateupdates(repo, wc, p2, pa,
668 670 branchmerge, force, partial)
669 671
670 672 ### apply phase
671 673 if not branchmerge: # just jump to the new rev
672 674 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
673 675 if not partial:
674 676 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
675 677
676 678 stats = applyupdates(repo, actions, wc, p2, pa, overwrite)
677 679
678 680 if not partial:
679 681 repo.setparents(fp1, fp2)
680 682 recordupdates(repo, actions, branchmerge)
681 683 if not branchmerge:
682 684 repo.dirstate.setbranch(p2.branch())
683 685 finally:
684 686 wlock.release()
685 687
686 688 if not partial:
687 689 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
688 690 return stats
General Comments 0
You need to be logged in to leave comments. Login now