##// END OF EJS Templates
manifestmerge: use dicthelpers.diff and join...
Siddharth Agarwal -
r18822:381c0ef7 default
parent child Browse files
Show More
@@ -1,724 +1,728
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 import error, util, filemerge, copies, subrepo, worker
10 import error, util, filemerge, copies, subrepo, worker, dicthelpers
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 acceptremote=False):
190 190 """
191 191 Merge p1 and p2 with ancestor pa and generate merge action list
192 192
193 193 branchmerge and force are as passed in to update
194 194 partial = function to filter file lists
195 195 acceptremote = accept the incoming changes without prompting
196 196 """
197 197
198 198 overwrite = force and not branchmerge
199 199 actions, copy, movewithdir = [], {}, {}
200 200
201 201 followcopies = False
202 202 if overwrite:
203 203 pa = wctx
204 204 elif pa == p2: # backwards
205 205 pa = wctx.p1()
206 206 elif not branchmerge and not wctx.dirty(missing=True):
207 207 pass
208 208 elif pa and repo.ui.configbool("merge", "followcopies", True):
209 209 followcopies = True
210 210
211 211 # manifests fetched in order are going to be faster, so prime the caches
212 212 [x.manifest() for x in
213 213 sorted(wctx.parents() + [p2, pa], key=lambda x: x.rev())]
214 214
215 215 if followcopies:
216 216 ret = copies.mergecopies(repo, wctx, p2, pa)
217 217 copy, movewithdir, diverge, renamedelete = ret
218 218 for of, fl in diverge.iteritems():
219 219 actions.append((of, "dr", (fl,), "divergent renames"))
220 220 for of, fl in renamedelete.iteritems():
221 221 actions.append((of, "rd", (fl,), "rename and delete"))
222 222
223 223 repo.ui.note(_("resolving manifests\n"))
224 224 repo.ui.debug(" branchmerge: %s, force: %s, partial: %s\n"
225 225 % (bool(branchmerge), bool(force), bool(partial)))
226 226 repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
227 227
228 228 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
229 229 copied = set(copy.values())
230 230 copied.update(movewithdir.values())
231 231
232 232 if '.hgsubstate' in m1:
233 233 # check whether sub state is modified
234 234 for s in sorted(wctx.substate):
235 235 if wctx.sub(s).dirty():
236 236 m1['.hgsubstate'] += "+"
237 237 break
238 238
239 239 aborts, prompts = [], []
240 240 # Compare manifests
241 for f, n1 in m1.iteritems():
241 fdiff = dicthelpers.diff(m1, m2)
242 flagsdiff = m1.flagsdiff(m2)
243 diff12 = dicthelpers.join(fdiff, flagsdiff)
244
245 for f, (n12, fl12) in diff12.iteritems():
246 if n12:
247 n1, n2 = n12
248 else: # file contents didn't change, but flags did
249 n1 = n2 = m1[f]
250 if fl12:
251 fl1, fl2 = fl12
252 else: # flags didn't change, file contents did
253 fl1 = fl2 = m1.flags(f)
254
242 255 if partial and not partial(f):
243 256 continue
244 if f in m2:
245 n2 = m2[f]
246 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
257 if n1 and n2:
258 fla = ma.flags(f)
247 259 nol = 'l' not in fl1 + fl2 + fla
248 260 a = ma.get(f, nullid)
249 if n1 == n2 and fl1 == fl2:
250 pass # same - keep local
251 elif n2 == a and fl2 == fla:
261 if n2 == a and fl2 == fla:
252 262 pass # remote unchanged - keep local
253 263 elif n1 == a and fl1 == fla: # local unchanged - use remote
254 264 if n1 == n2: # optimization: keep local content
255 265 actions.append((f, "e", (fl2,), "update permissions"))
256 266 else:
257 267 actions.append((f, "g", (fl2,), "remote is newer"))
258 268 elif nol and n2 == a: # remote only changed 'x'
259 269 actions.append((f, "e", (fl2,), "update permissions"))
260 270 elif nol and n1 == a: # local only changed 'x'
261 271 actions.append((f, "g", (fl1,), "remote is newer"))
262 272 else: # both changed something
263 273 actions.append((f, "m", (f, f, False), "versions differ"))
264 274 elif f in copied: # files we'll deal with on m2 side
265 275 pass
266 elif f in movewithdir: # directory rename
276 elif n1 and f in movewithdir: # directory rename
267 277 f2 = movewithdir[f]
268 278 actions.append((f, "d", (None, f2, m1.flags(f)),
269 279 "remote renamed directory to " + f2))
270 elif f in copy:
280 elif n1 and f in copy:
271 281 f2 = copy[f]
272 282 actions.append((f, "m", (f2, f, False),
273 283 "local copied/moved to " + f2))
274 elif f in ma: # clean, a different, no remote
284 elif n1 and f in ma: # clean, a different, no remote
275 285 if n1 != ma[f]:
276 286 prompts.append((f, "cd")) # prompt changed/deleted
277 287 elif n1[20:] == "a": # added, no remote
278 288 actions.append((f, "f", None, "remote deleted"))
279 289 else:
280 290 actions.append((f, "r", None, "other deleted"))
281
282 for f, n2 in m2.iteritems():
283 if partial and not partial(f):
284 continue
285 if f in m1 or f in copied: # files already visited
286 continue
287 if f in movewithdir:
291 elif n2 and f in movewithdir:
288 292 f2 = movewithdir[f]
289 293 actions.append((None, "d", (f, f2, m2.flags(f)),
290 294 "local renamed directory to " + f2))
291 elif f in copy:
295 elif n2 and f in copy:
292 296 f2 = copy[f]
293 297 if f2 in m2:
294 298 actions.append((f2, "m", (f, f, False),
295 299 "remote copied to " + f))
296 300 else:
297 301 actions.append((f2, "m", (f, f, True),
298 302 "remote moved to " + f))
299 elif f not in ma:
303 elif n2 and f not in ma:
300 304 # local unknown, remote created: the logic is described by the
301 305 # following table:
302 306 #
303 307 # force branchmerge different | action
304 308 # n * n | get
305 309 # n * y | abort
306 310 # y n * | get
307 311 # y y n | get
308 312 # y y y | merge
309 313 #
310 314 # Checking whether the files are different is expensive, so we
311 315 # don't do that when we can avoid it.
312 316 if force and not branchmerge:
313 317 actions.append((f, "g", (m2.flags(f),), "remote created"))
314 318 else:
315 319 different = _checkunknownfile(repo, wctx, p2, f)
316 320 if force and branchmerge and different:
317 321 actions.append((f, "m", (f, f, False),
318 322 "remote differs from untracked local"))
319 323 elif not force and different:
320 324 aborts.append((f, "ud"))
321 325 else:
322 326 actions.append((f, "g", (m2.flags(f),), "remote created"))
323 elif n2 != ma[f]:
327 elif n2 and n2 != ma[f]:
324 328 prompts.append((f, "dc")) # prompt deleted/changed
325 329
326 330 for f, m in sorted(aborts):
327 331 if m == "ud":
328 332 repo.ui.warn(_("%s: untracked file differs\n") % f)
329 333 else: assert False, m
330 334 if aborts:
331 335 raise util.Abort(_("untracked files in working directory differ "
332 336 "from files in requested revision"))
333 337
334 338 for f, m in sorted(prompts):
335 339 if m == "cd":
336 340 if acceptremote:
337 341 actions.append((f, "r", None, "remote delete"))
338 342 elif repo.ui.promptchoice(
339 343 _("local changed %s which remote deleted\n"
340 344 "use (c)hanged version or (d)elete?") % f,
341 345 (_("&Changed"), _("&Delete")), 0):
342 346 actions.append((f, "r", None, "prompt delete"))
343 347 else:
344 348 actions.append((f, "a", None, "prompt keep"))
345 349 elif m == "dc":
346 350 if acceptremote:
347 351 actions.append((f, "g", (m2.flags(f),), "remote recreating"))
348 352 elif repo.ui.promptchoice(
349 353 _("remote changed %s which local deleted\n"
350 354 "use (c)hanged version or leave (d)eleted?") % f,
351 355 (_("&Changed"), _("&Deleted")), 0) == 0:
352 356 actions.append((f, "g", (m2.flags(f),), "prompt recreating"))
353 357 else: assert False, m
354 358 return actions
355 359
356 360 def actionkey(a):
357 361 return a[1] == "r" and -1 or 0, a
358 362
359 363 def getremove(repo, mctx, overwrite, args):
360 364 """apply usually-non-interactive updates to the working directory
361 365
362 366 mctx is the context to be merged into the working copy
363 367
364 368 yields tuples for progress updates
365 369 """
366 370 verbose = repo.ui.verbose
367 371 unlink = util.unlinkpath
368 372 wjoin = repo.wjoin
369 373 fctx = mctx.filectx
370 374 wwrite = repo.wwrite
371 375 audit = repo.wopener.audit
372 376 i = 0
373 377 for arg in args:
374 378 f = arg[0]
375 379 if arg[1] == 'r':
376 380 if verbose:
377 381 repo.ui.note(_("removing %s\n") % f)
378 382 audit(f)
379 383 try:
380 384 unlink(wjoin(f), ignoremissing=True)
381 385 except OSError, inst:
382 386 repo.ui.warn(_("update failed to remove %s: %s!\n") %
383 387 (f, inst.strerror))
384 388 else:
385 389 if verbose:
386 390 repo.ui.note(_("getting %s\n") % f)
387 391 wwrite(f, fctx(f).data(), arg[2][0])
388 392 if i == 100:
389 393 yield i, f
390 394 i = 0
391 395 i += 1
392 396 if i > 0:
393 397 yield i, f
394 398
395 399 def applyupdates(repo, actions, wctx, mctx, actx, overwrite):
396 400 """apply the merge action list to the working directory
397 401
398 402 wctx is the working copy context
399 403 mctx is the context to be merged into the working copy
400 404 actx is the context of the common ancestor
401 405
402 406 Return a tuple of counts (updated, merged, removed, unresolved) that
403 407 describes how many files were affected by the update.
404 408 """
405 409
406 410 updated, merged, removed, unresolved = 0, 0, 0, 0
407 411 ms = mergestate(repo)
408 412 ms.reset(wctx.p1().node())
409 413 moves = []
410 414 actions.sort(key=actionkey)
411 415
412 416 # prescan for merges
413 417 for a in actions:
414 418 f, m, args, msg = a
415 419 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
416 420 if m == "m": # merge
417 421 f2, fd, move = args
418 422 if fd == '.hgsubstate': # merged internally
419 423 continue
420 424 repo.ui.debug(" preserving %s for resolve of %s\n" % (f, fd))
421 425 fcl = wctx[f]
422 426 fco = mctx[f2]
423 427 if mctx == actx: # backwards, use working dir parent as ancestor
424 428 if fcl.parents():
425 429 fca = fcl.p1()
426 430 else:
427 431 fca = repo.filectx(f, fileid=nullrev)
428 432 else:
429 433 fca = fcl.ancestor(fco, actx)
430 434 if not fca:
431 435 fca = repo.filectx(f, fileid=nullrev)
432 436 ms.add(fcl, fco, fca, fd)
433 437 if f != fd and move:
434 438 moves.append(f)
435 439
436 440 audit = repo.wopener.audit
437 441
438 442 # remove renamed files after safely stored
439 443 for f in moves:
440 444 if os.path.lexists(repo.wjoin(f)):
441 445 repo.ui.debug("removing %s\n" % f)
442 446 audit(f)
443 447 util.unlinkpath(repo.wjoin(f))
444 448
445 449 numupdates = len(actions)
446 450 workeractions = [a for a in actions if a[1] in 'gr']
447 451 updated = len([a for a in workeractions if a[1] == 'g'])
448 452 removed = len([a for a in workeractions if a[1] == 'r'])
449 453 actions = [a for a in actions if a[1] not in 'gr']
450 454
451 455 hgsub = [a[1] for a in workeractions if a[0] == '.hgsubstate']
452 456 if hgsub and hgsub[0] == 'r':
453 457 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
454 458
455 459 z = 0
456 460 prog = worker.worker(repo.ui, 0.001, getremove, (repo, mctx, overwrite),
457 461 workeractions)
458 462 for i, item in prog:
459 463 z += i
460 464 repo.ui.progress(_('updating'), z, item=item, total=numupdates,
461 465 unit=_('files'))
462 466
463 467 if hgsub and hgsub[0] == 'g':
464 468 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
465 469
466 470 _updating = _('updating')
467 471 _files = _('files')
468 472 progress = repo.ui.progress
469 473
470 474 for i, a in enumerate(actions):
471 475 f, m, args, msg = a
472 476 progress(_updating, z + i + 1, item=f, total=numupdates, unit=_files)
473 477 if m == "m": # merge
474 478 f2, fd, move = args
475 479 if fd == '.hgsubstate': # subrepo states need updating
476 480 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
477 481 overwrite)
478 482 continue
479 483 audit(fd)
480 484 r = ms.resolve(fd, wctx, mctx)
481 485 if r is not None and r > 0:
482 486 unresolved += 1
483 487 else:
484 488 if r is None:
485 489 updated += 1
486 490 else:
487 491 merged += 1
488 492 elif m == "d": # directory rename
489 493 f2, fd, flags = args
490 494 if f:
491 495 repo.ui.note(_("moving %s to %s\n") % (f, fd))
492 496 audit(f)
493 497 repo.wwrite(fd, wctx.filectx(f).data(), flags)
494 498 util.unlinkpath(repo.wjoin(f))
495 499 if f2:
496 500 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
497 501 repo.wwrite(fd, mctx.filectx(f2).data(), flags)
498 502 updated += 1
499 503 elif m == "dr": # divergent renames
500 504 fl, = args
501 505 repo.ui.warn(_("note: possible conflict - %s was renamed "
502 506 "multiple times to:\n") % f)
503 507 for nf in fl:
504 508 repo.ui.warn(" %s\n" % nf)
505 509 elif m == "rd": # rename and delete
506 510 fl, = args
507 511 repo.ui.warn(_("note: possible conflict - %s was deleted "
508 512 "and renamed to:\n") % f)
509 513 for nf in fl:
510 514 repo.ui.warn(" %s\n" % nf)
511 515 elif m == "e": # exec
512 516 flags, = args
513 517 audit(f)
514 518 util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
515 519 updated += 1
516 520 ms.commit()
517 521 progress(_updating, None, total=numupdates, unit=_files)
518 522
519 523 return updated, merged, removed, unresolved
520 524
521 525 def calculateupdates(repo, tctx, mctx, ancestor, branchmerge, force, partial,
522 526 acceptremote=False):
523 527 "Calculate the actions needed to merge mctx into tctx"
524 528 actions = []
525 529 folding = not util.checkcase(repo.path)
526 530 if folding:
527 531 # collision check is not needed for clean update
528 532 if (not branchmerge and
529 533 (force or not tctx.dirty(missing=True, branch=False))):
530 534 _checkcollision(mctx, None)
531 535 else:
532 536 _checkcollision(mctx, (tctx, ancestor))
533 537 actions += manifestmerge(repo, tctx, mctx,
534 538 ancestor,
535 539 branchmerge, force,
536 540 partial, acceptremote)
537 541 if tctx.rev() is None:
538 542 actions += _forgetremoved(tctx, mctx, branchmerge)
539 543 return actions
540 544
541 545 def recordupdates(repo, actions, branchmerge):
542 546 "record merge actions to the dirstate"
543 547
544 548 for a in actions:
545 549 f, m, args, msg = a
546 550 if m == "r": # remove
547 551 if branchmerge:
548 552 repo.dirstate.remove(f)
549 553 else:
550 554 repo.dirstate.drop(f)
551 555 elif m == "a": # re-add
552 556 if not branchmerge:
553 557 repo.dirstate.add(f)
554 558 elif m == "f": # forget
555 559 repo.dirstate.drop(f)
556 560 elif m == "e": # exec change
557 561 repo.dirstate.normallookup(f)
558 562 elif m == "g": # get
559 563 if branchmerge:
560 564 repo.dirstate.otherparent(f)
561 565 else:
562 566 repo.dirstate.normal(f)
563 567 elif m == "m": # merge
564 568 f2, fd, move = args
565 569 if branchmerge:
566 570 # We've done a branch merge, mark this file as merged
567 571 # so that we properly record the merger later
568 572 repo.dirstate.merge(fd)
569 573 if f != f2: # copy/rename
570 574 if move:
571 575 repo.dirstate.remove(f)
572 576 if f != fd:
573 577 repo.dirstate.copy(f, fd)
574 578 else:
575 579 repo.dirstate.copy(f2, fd)
576 580 else:
577 581 # We've update-merged a locally modified file, so
578 582 # we set the dirstate to emulate a normal checkout
579 583 # of that file some time in the past. Thus our
580 584 # merge will appear as a normal local file
581 585 # modification.
582 586 if f2 == fd: # file not locally copied/moved
583 587 repo.dirstate.normallookup(fd)
584 588 if move:
585 589 repo.dirstate.drop(f)
586 590 elif m == "d": # directory rename
587 591 f2, fd, flag = args
588 592 if not f2 and f not in repo.dirstate:
589 593 # untracked file moved
590 594 continue
591 595 if branchmerge:
592 596 repo.dirstate.add(fd)
593 597 if f:
594 598 repo.dirstate.remove(f)
595 599 repo.dirstate.copy(f, fd)
596 600 if f2:
597 601 repo.dirstate.copy(f2, fd)
598 602 else:
599 603 repo.dirstate.normal(fd)
600 604 if f:
601 605 repo.dirstate.drop(f)
602 606
603 607 def update(repo, node, branchmerge, force, partial, ancestor=None,
604 608 mergeancestor=False):
605 609 """
606 610 Perform a merge between the working directory and the given node
607 611
608 612 node = the node to update to, or None if unspecified
609 613 branchmerge = whether to merge between branches
610 614 force = whether to force branch merging or file overwriting
611 615 partial = a function to filter file lists (dirstate not updated)
612 616 mergeancestor = whether it is merging with an ancestor. If true,
613 617 we should accept the incoming changes for any prompts that occur.
614 618 If false, merging with an ancestor (fast-forward) is only allowed
615 619 between different named branches. This flag is used by rebase extension
616 620 as a temporary fix and should be avoided in general.
617 621
618 622 The table below shows all the behaviors of the update command
619 623 given the -c and -C or no options, whether the working directory
620 624 is dirty, whether a revision is specified, and the relationship of
621 625 the parent rev to the target rev (linear, on the same named
622 626 branch, or on another named branch).
623 627
624 628 This logic is tested by test-update-branches.t.
625 629
626 630 -c -C dirty rev | linear same cross
627 631 n n n n | ok (1) x
628 632 n n n y | ok ok ok
629 633 n n y * | merge (2) (2)
630 634 n y * * | --- discard ---
631 635 y n y * | --- (3) ---
632 636 y n n * | --- ok ---
633 637 y y * * | --- (4) ---
634 638
635 639 x = can't happen
636 640 * = don't-care
637 641 1 = abort: crosses branches (use 'hg merge' or 'hg update -c')
638 642 2 = abort: crosses branches (use 'hg merge' to merge or
639 643 use 'hg update -C' to discard changes)
640 644 3 = abort: uncommitted local changes
641 645 4 = incompatible options (checked in commands.py)
642 646
643 647 Return the same tuple as applyupdates().
644 648 """
645 649
646 650 onode = node
647 651 wlock = repo.wlock()
648 652 try:
649 653 wc = repo[None]
650 654 if node is None:
651 655 # tip of current branch
652 656 try:
653 657 node = repo.branchtip(wc.branch())
654 658 except error.RepoLookupError:
655 659 if wc.branch() == "default": # no default branch!
656 660 node = repo.lookup("tip") # update to tip
657 661 else:
658 662 raise util.Abort(_("branch %s not found") % wc.branch())
659 663 overwrite = force and not branchmerge
660 664 pl = wc.parents()
661 665 p1, p2 = pl[0], repo[node]
662 666 if ancestor:
663 667 pa = repo[ancestor]
664 668 else:
665 669 pa = p1.ancestor(p2)
666 670
667 671 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
668 672
669 673 ### check phase
670 674 if not overwrite and len(pl) > 1:
671 675 raise util.Abort(_("outstanding uncommitted merges"))
672 676 if branchmerge:
673 677 if pa == p2:
674 678 raise util.Abort(_("merging with a working directory ancestor"
675 679 " has no effect"))
676 680 elif pa == p1:
677 681 if not mergeancestor and p1.branch() == p2.branch():
678 682 raise util.Abort(_("nothing to merge"),
679 683 hint=_("use 'hg update' "
680 684 "or check 'hg heads'"))
681 685 if not force and (wc.files() or wc.deleted()):
682 686 raise util.Abort(_("outstanding uncommitted changes"),
683 687 hint=_("use 'hg status' to list changes"))
684 688 for s in sorted(wc.substate):
685 689 if wc.sub(s).dirty():
686 690 raise util.Abort(_("outstanding uncommitted changes in "
687 691 "subrepository '%s'") % s)
688 692
689 693 elif not overwrite:
690 694 if pa == p1 or pa == p2: # linear
691 695 pass # all good
692 696 elif wc.dirty(missing=True):
693 697 raise util.Abort(_("crosses branches (merge branches or use"
694 698 " --clean to discard changes)"))
695 699 elif onode is None:
696 700 raise util.Abort(_("crosses branches (merge branches or update"
697 701 " --check to force update)"))
698 702 else:
699 703 # Allow jumping branches if clean and specific rev given
700 704 pa = p1
701 705
702 706 ### calculate phase
703 707 actions = calculateupdates(repo, wc, p2, pa,
704 708 branchmerge, force, partial, mergeancestor)
705 709
706 710 ### apply phase
707 711 if not branchmerge: # just jump to the new rev
708 712 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
709 713 if not partial:
710 714 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
711 715
712 716 stats = applyupdates(repo, actions, wc, p2, pa, overwrite)
713 717
714 718 if not partial:
715 719 repo.setparents(fp1, fp2)
716 720 recordupdates(repo, actions, branchmerge)
717 721 if not branchmerge:
718 722 repo.dirstate.setbranch(p2.branch())
719 723 finally:
720 724 wlock.release()
721 725
722 726 if not partial:
723 727 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
724 728 return stats
General Comments 0
You need to be logged in to leave comments. Login now