##// END OF EJS Templates
merge: consider successor changesets for a bare update...
Sean Farley -
r20280:95b9c614 default
parent child Browse files
Show More
@@ -1,787 +1,815
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 from mercurial import obsolete
11 11 import error, util, filemerge, copies, subrepo, worker, dicthelpers
12 12 import errno, os, shutil
13 13
14 14 class mergestate(object):
15 15 '''track 3-way merge state of individual files'''
16 16 def __init__(self, repo):
17 17 self._repo = repo
18 18 self._dirty = False
19 19 self._read()
20 20 def reset(self, node=None):
21 21 self._state = {}
22 22 if node:
23 23 self._local = node
24 24 shutil.rmtree(self._repo.join("merge"), True)
25 25 self._dirty = False
26 26 def _read(self):
27 27 self._state = {}
28 28 try:
29 29 f = self._repo.opener("merge/state")
30 30 for i, l in enumerate(f):
31 31 if i == 0:
32 32 self._local = bin(l[:-1])
33 33 else:
34 34 bits = l[:-1].split("\0")
35 35 self._state[bits[0]] = bits[1:]
36 36 f.close()
37 37 except IOError, err:
38 38 if err.errno != errno.ENOENT:
39 39 raise
40 40 self._dirty = False
41 41 def commit(self):
42 42 if self._dirty:
43 43 f = self._repo.opener("merge/state", "w")
44 44 f.write(hex(self._local) + "\n")
45 45 for d, v in self._state.iteritems():
46 46 f.write("\0".join([d] + v) + "\n")
47 47 f.close()
48 48 self._dirty = False
49 49 def add(self, fcl, fco, fca, fd):
50 50 hash = util.sha1(fcl.path()).hexdigest()
51 51 self._repo.opener.write("merge/" + hash, fcl.data())
52 52 self._state[fd] = ['u', hash, fcl.path(), fca.path(),
53 53 hex(fca.filenode()), fco.path(), fcl.flags()]
54 54 self._dirty = True
55 55 def __contains__(self, dfile):
56 56 return dfile in self._state
57 57 def __getitem__(self, dfile):
58 58 return self._state[dfile][0]
59 59 def __iter__(self):
60 60 l = self._state.keys()
61 61 l.sort()
62 62 for f in l:
63 63 yield f
64 64 def files(self):
65 65 return self._state.keys()
66 66 def mark(self, dfile, state):
67 67 self._state[dfile][0] = state
68 68 self._dirty = True
69 69 def resolve(self, dfile, wctx, octx):
70 70 if self[dfile] == 'r':
71 71 return 0
72 72 state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
73 73 fcd = wctx[dfile]
74 74 fco = octx[ofile]
75 75 fca = self._repo.filectx(afile, fileid=anode)
76 76 # "premerge" x flags
77 77 flo = fco.flags()
78 78 fla = fca.flags()
79 79 if 'x' in flags + flo + fla and 'l' not in flags + flo + fla:
80 80 if fca.node() == nullid:
81 81 self._repo.ui.warn(_('warning: cannot merge flags for %s\n') %
82 82 afile)
83 83 elif flags == fla:
84 84 flags = flo
85 85 # restore local
86 86 f = self._repo.opener("merge/" + hash)
87 87 self._repo.wwrite(dfile, f.read(), flags)
88 88 f.close()
89 89 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
90 90 if r is None:
91 91 # no real conflict
92 92 del self._state[dfile]
93 93 elif not r:
94 94 self.mark(dfile, 'r')
95 95 return r
96 96
97 97 def _checkunknownfile(repo, wctx, mctx, f):
98 98 return (not repo.dirstate._ignore(f)
99 99 and os.path.isfile(repo.wjoin(f))
100 100 and repo.wopener.audit.check(f)
101 101 and repo.dirstate.normalize(f) not in repo.dirstate
102 102 and mctx[f].cmp(wctx[f]))
103 103
104 104 def _checkunknown(repo, wctx, mctx):
105 105 "check for collisions between unknown files and files in mctx"
106 106
107 107 error = False
108 108 for f in mctx:
109 109 if f not in wctx and _checkunknownfile(repo, wctx, mctx, f):
110 110 error = True
111 111 wctx._repo.ui.warn(_("%s: untracked file differs\n") % f)
112 112 if error:
113 113 raise util.Abort(_("untracked files in working directory differ "
114 114 "from files in requested revision"))
115 115
116 116 def _forgetremoved(wctx, mctx, branchmerge):
117 117 """
118 118 Forget removed files
119 119
120 120 If we're jumping between revisions (as opposed to merging), and if
121 121 neither the working directory nor the target rev has the file,
122 122 then we need to remove it from the dirstate, to prevent the
123 123 dirstate from listing the file when it is no longer in the
124 124 manifest.
125 125
126 126 If we're merging, and the other revision has removed a file
127 127 that is not present in the working directory, we need to mark it
128 128 as removed.
129 129 """
130 130
131 131 actions = []
132 132 state = branchmerge and 'r' or 'f'
133 133 for f in wctx.deleted():
134 134 if f not in mctx:
135 135 actions.append((f, state, None, "forget deleted"))
136 136
137 137 if not branchmerge:
138 138 for f in wctx.removed():
139 139 if f not in mctx:
140 140 actions.append((f, "f", None, "forget removed"))
141 141
142 142 return actions
143 143
144 144 def _checkcollision(repo, wmf, actions, prompts):
145 145 # build provisional merged manifest up
146 146 pmmf = set(wmf)
147 147
148 148 def addop(f, args):
149 149 pmmf.add(f)
150 150 def removeop(f, args):
151 151 pmmf.discard(f)
152 152 def nop(f, args):
153 153 pass
154 154
155 155 def renameop(f, args):
156 156 f2, fd, flags = args
157 157 if f:
158 158 pmmf.discard(f)
159 159 pmmf.add(fd)
160 160 def mergeop(f, args):
161 161 f2, fd, move = args
162 162 if move:
163 163 pmmf.discard(f)
164 164 pmmf.add(fd)
165 165
166 166 opmap = {
167 167 "a": addop,
168 168 "d": renameop,
169 169 "dr": nop,
170 170 "e": nop,
171 171 "f": addop, # untracked file should be kept in working directory
172 172 "g": addop,
173 173 "m": mergeop,
174 174 "r": removeop,
175 175 "rd": nop,
176 176 }
177 177 for f, m, args, msg in actions:
178 178 op = opmap.get(m)
179 179 assert op, m
180 180 op(f, args)
181 181
182 182 opmap = {
183 183 "cd": addop,
184 184 "dc": addop,
185 185 }
186 186 for f, m in prompts:
187 187 op = opmap.get(m)
188 188 assert op, m
189 189 op(f, None)
190 190
191 191 # check case-folding collision in provisional merged manifest
192 192 foldmap = {}
193 193 for f in sorted(pmmf):
194 194 fold = util.normcase(f)
195 195 if fold in foldmap:
196 196 raise util.Abort(_("case-folding collision between %s and %s")
197 197 % (f, foldmap[fold]))
198 198 foldmap[fold] = f
199 199
200 200 def manifestmerge(repo, wctx, p2, pa, branchmerge, force, partial,
201 201 acceptremote=False):
202 202 """
203 203 Merge p1 and p2 with ancestor pa and generate merge action list
204 204
205 205 branchmerge and force are as passed in to update
206 206 partial = function to filter file lists
207 207 acceptremote = accept the incoming changes without prompting
208 208 """
209 209
210 210 overwrite = force and not branchmerge
211 211 actions, copy, movewithdir = [], {}, {}
212 212
213 213 followcopies = False
214 214 if overwrite:
215 215 pa = wctx
216 216 elif pa == p2: # backwards
217 217 pa = wctx.p1()
218 218 elif not branchmerge and not wctx.dirty(missing=True):
219 219 pass
220 220 elif pa and repo.ui.configbool("merge", "followcopies", True):
221 221 followcopies = True
222 222
223 223 # manifests fetched in order are going to be faster, so prime the caches
224 224 [x.manifest() for x in
225 225 sorted(wctx.parents() + [p2, pa], key=lambda x: x.rev())]
226 226
227 227 if followcopies:
228 228 ret = copies.mergecopies(repo, wctx, p2, pa)
229 229 copy, movewithdir, diverge, renamedelete = ret
230 230 for of, fl in diverge.iteritems():
231 231 actions.append((of, "dr", (fl,), "divergent renames"))
232 232 for of, fl in renamedelete.iteritems():
233 233 actions.append((of, "rd", (fl,), "rename and delete"))
234 234
235 235 repo.ui.note(_("resolving manifests\n"))
236 236 repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n"
237 237 % (bool(branchmerge), bool(force), bool(partial)))
238 238 repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
239 239
240 240 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
241 241 copied = set(copy.values())
242 242 copied.update(movewithdir.values())
243 243
244 244 if '.hgsubstate' in m1:
245 245 # check whether sub state is modified
246 246 for s in sorted(wctx.substate):
247 247 if wctx.sub(s).dirty():
248 248 m1['.hgsubstate'] += "+"
249 249 break
250 250
251 251 aborts, prompts = [], []
252 252 # Compare manifests
253 253 fdiff = dicthelpers.diff(m1, m2)
254 254 flagsdiff = m1.flagsdiff(m2)
255 255 diff12 = dicthelpers.join(fdiff, flagsdiff)
256 256
257 257 for f, (n12, fl12) in diff12.iteritems():
258 258 if n12:
259 259 n1, n2 = n12
260 260 else: # file contents didn't change, but flags did
261 261 n1 = n2 = m1.get(f, None)
262 262 if n1 is None:
263 263 # Since n1 == n2, the file isn't present in m2 either. This
264 264 # means that the file was removed or deleted locally and
265 265 # removed remotely, but that residual entries remain in flags.
266 266 # This can happen in manifests generated by workingctx.
267 267 continue
268 268 if fl12:
269 269 fl1, fl2 = fl12
270 270 else: # flags didn't change, file contents did
271 271 fl1 = fl2 = m1.flags(f)
272 272
273 273 if partial and not partial(f):
274 274 continue
275 275 if n1 and n2:
276 276 fla = ma.flags(f)
277 277 nol = 'l' not in fl1 + fl2 + fla
278 278 a = ma.get(f, nullid)
279 279 if n2 == a and fl2 == fla:
280 280 pass # remote unchanged - keep local
281 281 elif n1 == a and fl1 == fla: # local unchanged - use remote
282 282 if n1 == n2: # optimization: keep local content
283 283 actions.append((f, "e", (fl2,), "update permissions"))
284 284 else:
285 285 actions.append((f, "g", (fl2,), "remote is newer"))
286 286 elif nol and n2 == a: # remote only changed 'x'
287 287 actions.append((f, "e", (fl2,), "update permissions"))
288 288 elif nol and n1 == a: # local only changed 'x'
289 289 actions.append((f, "g", (fl1,), "remote is newer"))
290 290 else: # both changed something
291 291 actions.append((f, "m", (f, f, False), "versions differ"))
292 292 elif f in copied: # files we'll deal with on m2 side
293 293 pass
294 294 elif n1 and f in movewithdir: # directory rename
295 295 f2 = movewithdir[f]
296 296 actions.append((f, "d", (None, f2, fl1),
297 297 "remote renamed directory to " + f2))
298 298 elif n1 and f in copy:
299 299 f2 = copy[f]
300 300 actions.append((f, "m", (f2, f, False),
301 301 "local copied/moved to " + f2))
302 302 elif n1 and f in ma: # clean, a different, no remote
303 303 if n1 != ma[f]:
304 304 prompts.append((f, "cd")) # prompt changed/deleted
305 305 elif n1[20:] == "a": # added, no remote
306 306 actions.append((f, "f", None, "remote deleted"))
307 307 else:
308 308 actions.append((f, "r", None, "other deleted"))
309 309 elif n2 and f in movewithdir:
310 310 f2 = movewithdir[f]
311 311 actions.append((None, "d", (f, f2, fl2),
312 312 "local renamed directory to " + f2))
313 313 elif n2 and f in copy:
314 314 f2 = copy[f]
315 315 if f2 in m2:
316 316 actions.append((f2, "m", (f, f, False),
317 317 "remote copied to " + f))
318 318 else:
319 319 actions.append((f2, "m", (f, f, True),
320 320 "remote moved to " + f))
321 321 elif n2 and f not in ma:
322 322 # local unknown, remote created: the logic is described by the
323 323 # following table:
324 324 #
325 325 # force branchmerge different | action
326 326 # n * n | get
327 327 # n * y | abort
328 328 # y n * | get
329 329 # y y n | get
330 330 # y y y | merge
331 331 #
332 332 # Checking whether the files are different is expensive, so we
333 333 # don't do that when we can avoid it.
334 334 if force and not branchmerge:
335 335 actions.append((f, "g", (fl2,), "remote created"))
336 336 else:
337 337 different = _checkunknownfile(repo, wctx, p2, f)
338 338 if force and branchmerge and different:
339 339 actions.append((f, "m", (f, f, False),
340 340 "remote differs from untracked local"))
341 341 elif not force and different:
342 342 aborts.append((f, "ud"))
343 343 else:
344 344 actions.append((f, "g", (fl2,), "remote created"))
345 345 elif n2 and n2 != ma[f]:
346 346 prompts.append((f, "dc")) # prompt deleted/changed
347 347
348 348 for f, m in sorted(aborts):
349 349 if m == "ud":
350 350 repo.ui.warn(_("%s: untracked file differs\n") % f)
351 351 else: assert False, m
352 352 if aborts:
353 353 raise util.Abort(_("untracked files in working directory differ "
354 354 "from files in requested revision"))
355 355
356 356 if not util.checkcase(repo.path):
357 357 # check collision between files only in p2 for clean update
358 358 if (not branchmerge and
359 359 (force or not wctx.dirty(missing=True, branch=False))):
360 360 _checkcollision(repo, m2, [], [])
361 361 else:
362 362 _checkcollision(repo, m1, actions, prompts)
363 363
364 364 for f, m in sorted(prompts):
365 365 if m == "cd":
366 366 if acceptremote:
367 367 actions.append((f, "r", None, "remote delete"))
368 368 elif repo.ui.promptchoice(
369 369 _("local changed %s which remote deleted\n"
370 370 "use (c)hanged version or (d)elete?"
371 371 "$$ &Changed $$ &Delete") % f, 0):
372 372 actions.append((f, "r", None, "prompt delete"))
373 373 else:
374 374 actions.append((f, "a", None, "prompt keep"))
375 375 elif m == "dc":
376 376 if acceptremote:
377 377 actions.append((f, "g", (m2.flags(f),), "remote recreating"))
378 378 elif repo.ui.promptchoice(
379 379 _("remote changed %s which local deleted\n"
380 380 "use (c)hanged version or leave (d)eleted?"
381 381 "$$ &Changed $$ &Deleted") % f, 0) == 0:
382 382 actions.append((f, "g", (m2.flags(f),), "prompt recreating"))
383 383 else: assert False, m
384 384 return actions
385 385
386 386 def actionkey(a):
387 387 return a[1] in "rf" and -1 or 0, a
388 388
389 389 def getremove(repo, mctx, overwrite, args):
390 390 """apply usually-non-interactive updates to the working directory
391 391
392 392 mctx is the context to be merged into the working copy
393 393
394 394 yields tuples for progress updates
395 395 """
396 396 verbose = repo.ui.verbose
397 397 unlink = util.unlinkpath
398 398 wjoin = repo.wjoin
399 399 fctx = mctx.filectx
400 400 wwrite = repo.wwrite
401 401 audit = repo.wopener.audit
402 402 i = 0
403 403 for arg in args:
404 404 f = arg[0]
405 405 if arg[1] == 'r':
406 406 if verbose:
407 407 repo.ui.note(_("removing %s\n") % f)
408 408 audit(f)
409 409 try:
410 410 unlink(wjoin(f), ignoremissing=True)
411 411 except OSError, inst:
412 412 repo.ui.warn(_("update failed to remove %s: %s!\n") %
413 413 (f, inst.strerror))
414 414 else:
415 415 if verbose:
416 416 repo.ui.note(_("getting %s\n") % f)
417 417 wwrite(f, fctx(f).data(), arg[2][0])
418 418 if i == 100:
419 419 yield i, f
420 420 i = 0
421 421 i += 1
422 422 if i > 0:
423 423 yield i, f
424 424
425 425 def applyupdates(repo, actions, wctx, mctx, actx, overwrite):
426 426 """apply the merge action list to the working directory
427 427
428 428 wctx is the working copy context
429 429 mctx is the context to be merged into the working copy
430 430 actx is the context of the common ancestor
431 431
432 432 Return a tuple of counts (updated, merged, removed, unresolved) that
433 433 describes how many files were affected by the update.
434 434 """
435 435
436 436 updated, merged, removed, unresolved = 0, 0, 0, 0
437 437 ms = mergestate(repo)
438 438 ms.reset(wctx.p1().node())
439 439 moves = []
440 440 actions.sort(key=actionkey)
441 441
442 442 # prescan for merges
443 443 for a in actions:
444 444 f, m, args, msg = a
445 445 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
446 446 if m == "m": # merge
447 447 f2, fd, move = args
448 448 if fd == '.hgsubstate': # merged internally
449 449 continue
450 450 repo.ui.debug(" preserving %s for resolve of %s\n" % (f, fd))
451 451 fcl = wctx[f]
452 452 fco = mctx[f2]
453 453 if mctx == actx: # backwards, use working dir parent as ancestor
454 454 if fcl.parents():
455 455 fca = fcl.p1()
456 456 else:
457 457 fca = repo.filectx(f, fileid=nullrev)
458 458 else:
459 459 fca = fcl.ancestor(fco, actx)
460 460 if not fca:
461 461 fca = repo.filectx(f, fileid=nullrev)
462 462 ms.add(fcl, fco, fca, fd)
463 463 if f != fd and move:
464 464 moves.append(f)
465 465
466 466 audit = repo.wopener.audit
467 467
468 468 # remove renamed files after safely stored
469 469 for f in moves:
470 470 if os.path.lexists(repo.wjoin(f)):
471 471 repo.ui.debug("removing %s\n" % f)
472 472 audit(f)
473 473 util.unlinkpath(repo.wjoin(f))
474 474
475 475 numupdates = len(actions)
476 476 workeractions = [a for a in actions if a[1] in 'gr']
477 477 updateactions = [a for a in workeractions if a[1] == 'g']
478 478 updated = len(updateactions)
479 479 removeactions = [a for a in workeractions if a[1] == 'r']
480 480 removed = len(removeactions)
481 481 actions = [a for a in actions if a[1] not in 'gr']
482 482
483 483 hgsub = [a[1] for a in workeractions if a[0] == '.hgsubstate']
484 484 if hgsub and hgsub[0] == 'r':
485 485 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
486 486
487 487 z = 0
488 488 prog = worker.worker(repo.ui, 0.001, getremove, (repo, mctx, overwrite),
489 489 removeactions)
490 490 for i, item in prog:
491 491 z += i
492 492 repo.ui.progress(_('updating'), z, item=item, total=numupdates,
493 493 unit=_('files'))
494 494 prog = worker.worker(repo.ui, 0.001, getremove, (repo, mctx, overwrite),
495 495 updateactions)
496 496 for i, item in prog:
497 497 z += i
498 498 repo.ui.progress(_('updating'), z, item=item, total=numupdates,
499 499 unit=_('files'))
500 500
501 501 if hgsub and hgsub[0] == 'g':
502 502 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
503 503
504 504 _updating = _('updating')
505 505 _files = _('files')
506 506 progress = repo.ui.progress
507 507
508 508 for i, a in enumerate(actions):
509 509 f, m, args, msg = a
510 510 progress(_updating, z + i + 1, item=f, total=numupdates, unit=_files)
511 511 if m == "m": # merge
512 512 f2, fd, move = args
513 513 if fd == '.hgsubstate': # subrepo states need updating
514 514 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
515 515 overwrite)
516 516 continue
517 517 audit(fd)
518 518 r = ms.resolve(fd, wctx, mctx)
519 519 if r is not None and r > 0:
520 520 unresolved += 1
521 521 else:
522 522 if r is None:
523 523 updated += 1
524 524 else:
525 525 merged += 1
526 526 elif m == "d": # directory rename
527 527 f2, fd, flags = args
528 528 if f:
529 529 repo.ui.note(_("moving %s to %s\n") % (f, fd))
530 530 audit(f)
531 531 repo.wwrite(fd, wctx.filectx(f).data(), flags)
532 532 util.unlinkpath(repo.wjoin(f))
533 533 if f2:
534 534 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
535 535 repo.wwrite(fd, mctx.filectx(f2).data(), flags)
536 536 updated += 1
537 537 elif m == "dr": # divergent renames
538 538 fl, = args
539 539 repo.ui.warn(_("note: possible conflict - %s was renamed "
540 540 "multiple times to:\n") % f)
541 541 for nf in fl:
542 542 repo.ui.warn(" %s\n" % nf)
543 543 elif m == "rd": # rename and delete
544 544 fl, = args
545 545 repo.ui.warn(_("note: possible conflict - %s was deleted "
546 546 "and renamed to:\n") % f)
547 547 for nf in fl:
548 548 repo.ui.warn(" %s\n" % nf)
549 549 elif m == "e": # exec
550 550 flags, = args
551 551 audit(f)
552 552 util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
553 553 updated += 1
554 554 ms.commit()
555 555 progress(_updating, None, total=numupdates, unit=_files)
556 556
557 557 return updated, merged, removed, unresolved
558 558
559 559 def calculateupdates(repo, tctx, mctx, ancestor, branchmerge, force, partial,
560 560 acceptremote=False):
561 561 "Calculate the actions needed to merge mctx into tctx"
562 562 actions = []
563 563 actions += manifestmerge(repo, tctx, mctx,
564 564 ancestor,
565 565 branchmerge, force,
566 566 partial, acceptremote)
567 567 if tctx.rev() is None:
568 568 actions += _forgetremoved(tctx, mctx, branchmerge)
569 569 return actions
570 570
571 571 def recordupdates(repo, actions, branchmerge):
572 572 "record merge actions to the dirstate"
573 573
574 574 for a in actions:
575 575 f, m, args, msg = a
576 576 if m == "r": # remove
577 577 if branchmerge:
578 578 repo.dirstate.remove(f)
579 579 else:
580 580 repo.dirstate.drop(f)
581 581 elif m == "a": # re-add
582 582 if not branchmerge:
583 583 repo.dirstate.add(f)
584 584 elif m == "f": # forget
585 585 repo.dirstate.drop(f)
586 586 elif m == "e": # exec change
587 587 repo.dirstate.normallookup(f)
588 588 elif m == "g": # get
589 589 if branchmerge:
590 590 repo.dirstate.otherparent(f)
591 591 else:
592 592 repo.dirstate.normal(f)
593 593 elif m == "m": # merge
594 594 f2, fd, move = args
595 595 if branchmerge:
596 596 # We've done a branch merge, mark this file as merged
597 597 # so that we properly record the merger later
598 598 repo.dirstate.merge(fd)
599 599 if f != f2: # copy/rename
600 600 if move:
601 601 repo.dirstate.remove(f)
602 602 if f != fd:
603 603 repo.dirstate.copy(f, fd)
604 604 else:
605 605 repo.dirstate.copy(f2, fd)
606 606 else:
607 607 # We've update-merged a locally modified file, so
608 608 # we set the dirstate to emulate a normal checkout
609 609 # of that file some time in the past. Thus our
610 610 # merge will appear as a normal local file
611 611 # modification.
612 612 if f2 == fd: # file not locally copied/moved
613 613 repo.dirstate.normallookup(fd)
614 614 if move:
615 615 repo.dirstate.drop(f)
616 616 elif m == "d": # directory rename
617 617 f2, fd, flag = args
618 618 if not f2 and f not in repo.dirstate:
619 619 # untracked file moved
620 620 continue
621 621 if branchmerge:
622 622 repo.dirstate.add(fd)
623 623 if f:
624 624 repo.dirstate.remove(f)
625 625 repo.dirstate.copy(f, fd)
626 626 if f2:
627 627 repo.dirstate.copy(f2, fd)
628 628 else:
629 629 repo.dirstate.normal(fd)
630 630 if f:
631 631 repo.dirstate.drop(f)
632 632
633 633 def update(repo, node, branchmerge, force, partial, ancestor=None,
634 634 mergeancestor=False):
635 635 """
636 636 Perform a merge between the working directory and the given node
637 637
638 638 node = the node to update to, or None if unspecified
639 639 branchmerge = whether to merge between branches
640 640 force = whether to force branch merging or file overwriting
641 641 partial = a function to filter file lists (dirstate not updated)
642 642 mergeancestor = whether it is merging with an ancestor. If true,
643 643 we should accept the incoming changes for any prompts that occur.
644 644 If false, merging with an ancestor (fast-forward) is only allowed
645 645 between different named branches. This flag is used by rebase extension
646 646 as a temporary fix and should be avoided in general.
647 647
648 648 The table below shows all the behaviors of the update command
649 649 given the -c and -C or no options, whether the working directory
650 650 is dirty, whether a revision is specified, and the relationship of
651 651 the parent rev to the target rev (linear, on the same named
652 652 branch, or on another named branch).
653 653
654 654 This logic is tested by test-update-branches.t.
655 655
656 656 -c -C dirty rev | linear same cross
657 657 n n n n | ok (1) x
658 658 n n n y | ok ok ok
659 659 n n y n | merge (2) (2)
660 660 n n y y | merge (3) (3)
661 661 n y * * | --- discard ---
662 662 y n y * | --- (4) ---
663 663 y n n * | --- ok ---
664 664 y y * * | --- (5) ---
665 665
666 666 x = can't happen
667 667 * = don't-care
668 668 1 = abort: not a linear update (merge or update --check to force update)
669 669 2 = abort: uncommitted changes (commit and merge, or update --clean to
670 670 discard changes)
671 671 3 = abort: uncommitted changes (commit or update --clean to discard changes)
672 672 4 = abort: uncommitted changes (checked in commands.py)
673 673 5 = incompatible options (checked in commands.py)
674 674
675 675 Return the same tuple as applyupdates().
676 676 """
677 677
678 678 onode = node
679 679 wlock = repo.wlock()
680 680 try:
681 681 wc = repo[None]
682 682 pl = wc.parents()
683 683 p1 = pl[0]
684 684 pa = None
685 685 if ancestor:
686 686 pa = repo[ancestor]
687 687
688 688 if node is None:
689 689 # Here is where we should consider bookmarks, divergent bookmarks,
690 690 # foreground changesets (successors), and tip of current branch;
691 691 # but currently we are only checking the branch tips.
692 692 try:
693 693 node = repo.branchtip(wc.branch())
694 694 except error.RepoLookupError:
695 695 if wc.branch() == "default": # no default branch!
696 696 node = repo.lookup("tip") # update to tip
697 697 else:
698 698 raise util.Abort(_("branch %s not found") % wc.branch())
699
700 if p1.obsolete() and not p1.children():
701 # allow updating to successors
702 successors = obsolete.successorssets(repo, p1.node())
703
704 # behavior of certain cases is as follows,
705 #
706 # divergent changesets: update to highest rev, similar to what
707 # is currently done when there are more than one head
708 # (i.e. 'tip')
709 #
710 # replaced changesets: same as divergent except we know there
711 # is no conflict
712 #
713 # pruned changeset: no update is done; though, we could
714 # consider updating to the first non-obsolete parent,
715 # similar to what is current done for 'hg prune'
716
717 if successors:
718 # flatten the list here handles both divergent (len > 1)
719 # and the usual case (len = 1)
720 successors = [n for sub in successors for n in sub]
721
722 # get the max revision for the given successors set,
723 # i.e. the 'tip' of a set
724 node = repo.revs("max(%ln)", successors)[0]
725 pa = p1
726
699 727 overwrite = force and not branchmerge
700 728
701 729 p2 = repo[node]
702 730 if pa is None:
703 731 pa = p1.ancestor(p2)
704 732
705 733 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
706 734
707 735 ### check phase
708 736 if not overwrite and len(pl) > 1:
709 737 raise util.Abort(_("outstanding uncommitted merges"))
710 738 if branchmerge:
711 739 if pa == p2:
712 740 raise util.Abort(_("merging with a working directory ancestor"
713 741 " has no effect"))
714 742 elif pa == p1:
715 743 if not mergeancestor and p1.branch() == p2.branch():
716 744 raise util.Abort(_("nothing to merge"),
717 745 hint=_("use 'hg update' "
718 746 "or check 'hg heads'"))
719 747 if not force and (wc.files() or wc.deleted()):
720 748 raise util.Abort(_("uncommitted changes"),
721 749 hint=_("use 'hg status' to list changes"))
722 750 for s in sorted(wc.substate):
723 751 if wc.sub(s).dirty():
724 752 raise util.Abort(_("uncommitted changes in "
725 753 "subrepository '%s'") % s)
726 754
727 755 elif not overwrite:
728 756 if p1 == p2: # no-op update
729 757 # call the hooks and exit early
730 758 repo.hook('preupdate', throw=True, parent1=xp2, parent2='')
731 759 repo.hook('update', parent1=xp2, parent2='', error=0)
732 760 return 0, 0, 0, 0
733 761
734 762 if pa not in (p1, p2): # nonlinear
735 763 dirty = wc.dirty(missing=True)
736 764 if dirty or onode is None:
737 765 # Branching is a bit strange to ensure we do the minimal
738 766 # amount of call to obsolete.background.
739 767 foreground = obsolete.foreground(repo, [p1.node()])
740 768 # note: the <node> variable contains a random identifier
741 769 if repo[node].node() in foreground:
742 770 pa = p1 # allow updating to successors
743 771 elif dirty:
744 772 msg = _("uncommitted changes")
745 773 if onode is None:
746 774 hint = _("commit and merge, or update --clean to"
747 775 " discard changes")
748 776 else:
749 777 hint = _("commit or update --clean to discard"
750 778 " changes")
751 779 raise util.Abort(msg, hint=hint)
752 780 else: # node is none
753 781 msg = _("not a linear update")
754 782 hint = _("merge or update --check to force update")
755 783 raise util.Abort(msg, hint=hint)
756 784 else:
757 785 # Allow jumping branches if clean and specific rev given
758 786 pa = p1
759 787
760 788 ### calculate phase
761 789 actions = calculateupdates(repo, wc, p2, pa,
762 790 branchmerge, force, partial, mergeancestor)
763 791
764 792 ### apply phase
765 793 if not branchmerge: # just jump to the new rev
766 794 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
767 795 if not partial:
768 796 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
769 797 # note that we're in the middle of an update
770 798 repo.vfs.write('updatestate', p2.hex())
771 799
772 800 stats = applyupdates(repo, actions, wc, p2, pa, overwrite)
773 801
774 802 if not partial:
775 803 repo.setparents(fp1, fp2)
776 804 recordupdates(repo, actions, branchmerge)
777 805 # update completed, clear state
778 806 util.unlink(repo.join('updatestate'))
779 807
780 808 if not branchmerge:
781 809 repo.dirstate.setbranch(p2.branch())
782 810 finally:
783 811 wlock.release()
784 812
785 813 if not partial:
786 814 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
787 815 return stats
@@ -1,244 +1,263
1 1 # Construct the following history tree:
2 2 #
3 3 # @ 5:e1bb631146ca b1
4 4 # |
5 5 # o 4:a4fdb3b883c4 0:b608b9236435 b1
6 6 # |
7 7 # | o 3:4b57d2520816 1:44592833ba9f
8 8 # | |
9 9 # | | o 2:063f31070f65
10 10 # | |/
11 11 # | o 1:44592833ba9f
12 12 # |/
13 13 # o 0:b608b9236435
14 14
15 15 $ mkdir b1
16 16 $ cd b1
17 17 $ hg init
18 18 $ echo foo > foo
19 19 $ echo zero > a
20 20 $ hg init sub
21 21 $ echo suba > sub/suba
22 22 $ hg --cwd sub ci -Am addsuba
23 23 adding suba
24 24 $ echo 'sub = sub' > .hgsub
25 25 $ hg ci -qAm0
26 26 $ echo one > a ; hg ci -m1
27 27 $ echo two > a ; hg ci -m2
28 28 $ hg up -q 1
29 29 $ echo three > a ; hg ci -qm3
30 30 $ hg up -q 0
31 31 $ hg branch -q b1
32 32 $ echo four > a ; hg ci -qm4
33 33 $ echo five > a ; hg ci -qm5
34 34
35 35 Initial repo state:
36 36
37 37 $ hg log -G --template '{rev}:{node|short} {parents} {branches}\n'
38 38 @ 5:ff252e8273df b1
39 39 |
40 40 o 4:d047485b3896 0:60829823a42a b1
41 41 |
42 42 | o 3:6efa171f091b 1:0786582aa4b1
43 43 | |
44 44 | | o 2:bd10386d478c
45 45 | |/
46 46 | o 1:0786582aa4b1
47 47 |/
48 48 o 0:60829823a42a
49 49
50 50
51 51 Make sure update doesn't assume b1 is a repository if invoked from outside:
52 52
53 53 $ cd ..
54 54 $ hg update b1
55 55 abort: no repository found in '$TESTTMP' (.hg not found)!
56 56 [255]
57 57 $ cd b1
58 58
59 59 Test helper functions:
60 60
61 61 $ revtest () {
62 62 > msg=$1
63 63 > dirtyflag=$2 # 'clean', 'dirty' or 'dirtysub'
64 64 > startrev=$3
65 65 > targetrev=$4
66 66 > opt=$5
67 67 > hg up -qC $startrev
68 68 > test $dirtyflag = dirty && echo dirty > foo
69 69 > test $dirtyflag = dirtysub && echo dirty > sub/suba
70 70 > hg up $opt $targetrev
71 71 > hg parent --template 'parent={rev}\n'
72 72 > hg stat -S
73 73 > }
74 74
75 75 $ norevtest () {
76 76 > msg=$1
77 77 > dirtyflag=$2 # 'clean', 'dirty' or 'dirtysub'
78 78 > startrev=$3
79 79 > opt=$4
80 80 > hg up -qC $startrev
81 81 > test $dirtyflag = dirty && echo dirty > foo
82 82 > test $dirtyflag = dirtysub && echo dirty > sub/suba
83 83 > hg up $opt
84 84 > hg parent --template 'parent={rev}\n'
85 85 > hg stat -S
86 86 > }
87 87
88 88 Test cases are documented in a table in the update function of merge.py.
89 89 Cases are run as shown in that table, row by row.
90 90
91 91 $ norevtest 'none clean linear' clean 4
92 92 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
93 93 parent=5
94 94
95 95 $ norevtest 'none clean same' clean 2
96 96 abort: not a linear update
97 97 (merge or update --check to force update)
98 98 parent=2
99 99
100 100
101 101 $ revtest 'none clean linear' clean 1 2
102 102 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
103 103 parent=2
104 104
105 105 $ revtest 'none clean same' clean 2 3
106 106 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
107 107 parent=3
108 108
109 109 $ revtest 'none clean cross' clean 3 4
110 110 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
111 111 parent=4
112 112
113 113
114 114 $ revtest 'none dirty linear' dirty 1 2
115 115 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
116 116 parent=2
117 117 M foo
118 118
119 119 $ revtest 'none dirtysub linear' dirtysub 1 2
120 120 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
121 121 parent=2
122 122 M sub/suba
123 123
124 124 $ revtest 'none dirty same' dirty 2 3
125 125 abort: uncommitted changes
126 126 (commit or update --clean to discard changes)
127 127 parent=2
128 128 M foo
129 129
130 130 $ revtest 'none dirtysub same' dirtysub 2 3
131 131 abort: uncommitted changes
132 132 (commit or update --clean to discard changes)
133 133 parent=2
134 134 M sub/suba
135 135
136 136 $ revtest 'none dirty cross' dirty 3 4
137 137 abort: uncommitted changes
138 138 (commit or update --clean to discard changes)
139 139 parent=3
140 140 M foo
141 141
142 142 $ norevtest 'none dirty cross' dirty 2
143 143 abort: uncommitted changes
144 144 (commit and merge, or update --clean to discard changes)
145 145 parent=2
146 146 M foo
147 147
148 148 $ revtest 'none dirtysub cross' dirtysub 3 4
149 149 abort: uncommitted changes
150 150 (commit or update --clean to discard changes)
151 151 parent=3
152 152 M sub/suba
153 153
154 154 $ revtest '-C dirty linear' dirty 1 2 -C
155 155 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
156 156 parent=2
157 157
158 158 $ revtest '-c dirty linear' dirty 1 2 -c
159 159 abort: uncommitted changes
160 160 parent=1
161 161 M foo
162 162
163 163 $ revtest '-c dirtysub linear' dirtysub 1 2 -c
164 164 abort: uncommitted changes
165 165 parent=1
166 166 M sub/suba
167 167
168 168 $ norevtest '-c clean same' clean 2 -c
169 169 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
170 170 parent=3
171 171
172 172 $ revtest '-cC dirty linear' dirty 1 2 -cC
173 173 abort: cannot specify both -c/--check and -C/--clean
174 174 parent=1
175 175 M foo
176 176
177 177 Test obsolescence behavior
178 178 ---------------------------------------------------------------------
179 179
180 180 successors should be taken in account when checking head destination
181 181
182 182 $ cat << EOF >> $HGRCPATH
183 183 > [extensions]
184 184 > obs=$TESTTMP/obs.py
185 185 > [ui]
186 186 > logtemplate={rev}:{node|short} {desc|firstline}
187 187 > EOF
188 188 $ cat > $TESTTMP/obs.py << EOF
189 189 > import mercurial.obsolete
190 190 > mercurial.obsolete._enabled = True
191 191 > EOF
192 192
193 193 Test no-argument update to a successor of an obsoleted changeset
194 194
195 195 $ hg log -G
196 196 o 5:ff252e8273df 5
197 197 |
198 198 o 4:d047485b3896 4
199 199 |
200 200 | o 3:6efa171f091b 3
201 201 | |
202 202 | | o 2:bd10386d478c 2
203 203 | |/
204 204 | @ 1:0786582aa4b1 1
205 205 |/
206 206 o 0:60829823a42a 0
207 207
208 208 $ hg book bm -r 3
209 209 $ hg status
210 210 M foo
211 211
212 212 We add simple obsolescence marker between 3 and 4 (indirect successors)
213 213
214 214 $ hg id --debug -i -r 3
215 215 6efa171f091b00a3c35edc15d48c52a498929953
216 216 $ hg id --debug -i -r 4
217 217 d047485b3896813b2a624e86201983520f003206
218 218 $ hg debugobsolete 6efa171f091b00a3c35edc15d48c52a498929953 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
219 219 $ hg debugobsolete aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa d047485b3896813b2a624e86201983520f003206
220 220
221 221 Test that 5 is detected as a valid destination from 3 and also accepts moving
222 222 the bookmark (issue4015)
223 223
224 224 $ hg up --quiet --hidden 3
225 225 $ hg up 5
226 226 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
227 227 $ hg book bm
228 228 moving bookmark 'bm' forward from 6efa171f091b
229 229 $ hg bookmarks
230 230 * bm 5:ff252e8273df
231 231
232 Test that 4 is detected as the no-argument destination from 3
233 $ hg up --quiet 0 # we should be able to update to 3 directly
234 $ hg up --quiet --hidden 3 # but not implemented yet.
235 $ hg up
236 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 $ hg id
238 d047485b3896+ (b1)
239
232 240 Test that 5 is detected as a valid destination from 1
233 241 $ hg up --quiet 0 # we should be able to update to 3 directly
234 242 $ hg up --quiet --hidden 3 # but not implemented yet.
235 243 $ hg up 5
236 244 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 245
238 246 Test that 5 is not detected as a valid destination from 2
239 247 $ hg up --quiet 0
240 248 $ hg up --quiet 2
241 249 $ hg up 5
242 250 abort: uncommitted changes
243 251 (commit or update --clean to discard changes)
244 252 [255]
253
254 Test that we don't crash when updating from a pruned changeset (i.e. has no
255 successors). Behavior should probably be that we update to the first
256 non-obsolete parent but that will be decided later.
257 $ hg id --debug -r 2
258 bd10386d478cd5a9faf2e604114c8e6da62d3889
259 $ hg up --quiet 0
260 $ hg up --quiet 2
261 $ hg debugobsolete bd10386d478cd5a9faf2e604114c8e6da62d3889
262 $ hg up
263 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
General Comments 0
You need to be logged in to leave comments. Login now