##// END OF EJS Templates
merge: simplify checkcopies
Matt Mackall -
r6270:14f0fe2e default
parent child Browse files
Show More
@@ -1,641 +1,642 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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import nullid, nullrev
9 9 from i18n import _
10 10 import errno, util, os, heapq, filemerge
11 11
12 12 def _checkunknown(wctx, mctx):
13 13 "check for collisions between unknown files and files in mctx"
14 14 man = mctx.manifest()
15 15 for f in wctx.unknown():
16 16 if f in man:
17 17 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
18 18 raise util.Abort(_("untracked file in working directory differs"
19 19 " from file in requested revision: '%s'")
20 20 % f)
21 21
22 22 def _checkcollision(mctx):
23 23 "check for case folding collisions in the destination context"
24 24 folded = {}
25 25 for fn in mctx.manifest():
26 26 fold = fn.lower()
27 27 if fold in folded:
28 28 raise util.Abort(_("case-folding collision between %s and %s")
29 29 % (fn, folded[fold]))
30 30 folded[fold] = fn
31 31
32 32 def _forgetremoved(wctx, mctx, branchmerge):
33 33 """
34 34 Forget removed files
35 35
36 36 If we're jumping between revisions (as opposed to merging), and if
37 37 neither the working directory nor the target rev has the file,
38 38 then we need to remove it from the dirstate, to prevent the
39 39 dirstate from listing the file when it is no longer in the
40 40 manifest.
41 41
42 42 If we're merging, and the other revision has removed a file
43 43 that is not present in the working directory, we need to mark it
44 44 as removed.
45 45 """
46 46
47 47 action = []
48 48 man = mctx.manifest()
49 49 state = branchmerge and 'r' or 'f'
50 50 for f in wctx.deleted():
51 51 if f not in man:
52 52 action.append((f, state))
53 53
54 54 if not branchmerge:
55 55 for f in wctx.removed():
56 56 if f not in man:
57 57 action.append((f, "f"))
58 58
59 59 return action
60 60
61 61 def _nonoverlap(d1, d2, d3):
62 62 "Return list of elements in d1 not in d2 or d3"
63 63 l = [d for d in d1 if d not in d3 and d not in d2]
64 64 l.sort()
65 65 return l
66 66
67 67 def _dirname(f):
68 68 s = f.rfind("/")
69 69 if s == -1:
70 70 return ""
71 71 return f[:s]
72 72
73 73 def _dirs(files):
74 74 d = {}
75 75 for f in files:
76 76 f = _dirname(f)
77 77 while f not in d:
78 78 d[f] = True
79 79 f = _dirname(f)
80 80 return d
81 81
82 82 def _findoldnames(fctx, limit):
83 83 "find files that path was copied from, back to linkrev limit"
84 84 old = {}
85 85 seen = {}
86 86 orig = fctx.path()
87 87 visit = [fctx]
88 88 while visit:
89 89 fc = visit.pop()
90 90 s = str(fc)
91 91 if s in seen:
92 92 continue
93 93 seen[s] = 1
94 94 if fc.path() != orig and fc.path() not in old:
95 95 old[fc.path()] = 1
96 96 if fc.rev() < limit:
97 97 continue
98 98 visit += fc.parents()
99 99
100 100 old = old.keys()
101 101 old.sort()
102 102 return old
103 103
104 104 def findcopies(repo, m1, m2, ma, limit):
105 105 """
106 106 Find moves and copies between m1 and m2 back to limit linkrev
107 107 """
108 108
109 109 wctx = repo.workingctx()
110 110
111 111 def makectx(f, n):
112 112 if len(n) == 20:
113 113 return repo.filectx(f, fileid=n)
114 114 return wctx.filectx(f)
115 115 ctx = util.cachefunc(makectx)
116 116
117 117 copy = {}
118 118 fullcopy = {}
119 119 diverge = {}
120 120
121 def checkcopies(c, man, aman):
122 '''check possible copies for filectx c'''
123 for of in _findoldnames(c, limit):
124 fullcopy[c.path()] = of # remember for dir rename detection
125 if of not in man: # original file not in other manifest?
121 def checkcopies(f, m1, m2):
122 '''check possible copies of f from m1 to m2'''
123 c1 = ctx(f, m1[f])
124 for of in _findoldnames(c1, limit):
125 fullcopy[f] = of # remember for dir rename detection
126 if of not in m2: # original file not in other manifest?
126 127 if of in ma:
127 diverge.setdefault(of, []).append(c.path())
128 diverge.setdefault(of, []).append(f)
128 129 continue
129 130 # if the original file is unchanged on the other branch,
130 131 # no merge needed
131 if man[of] == aman.get(of):
132 if m2[of] == ma.get(of):
132 133 continue
133 c2 = ctx(of, man[of])
134 ca = c.ancestor(c2)
134 c2 = ctx(of, m2[of])
135 ca = c1.ancestor(c2)
135 136 if not ca: # unrelated?
136 137 continue
137 138 # named changed on only one side?
138 if ca.path() == c.path() or ca.path() == c2.path():
139 if c == ca and c2 == ca: # no merge needed, ignore copy
139 if ca.path() == f or ca.path() == c2.path():
140 if c1 == ca and c2 == ca: # no merge needed, ignore copy
140 141 continue
141 copy[c.path()] = of
142 copy[f] = of
142 143
143 144 if not repo.ui.configbool("merge", "followcopies", True):
144 145 return {}, {}
145 146
146 147 # avoid silly behavior for update from empty dir
147 148 if not m1 or not m2 or not ma:
148 149 return {}, {}
149 150
150 151 repo.ui.debug(_(" searching for copies back to rev %d\n") % limit)
151 152
152 153 u1 = _nonoverlap(m1, m2, ma)
153 154 u2 = _nonoverlap(m2, m1, ma)
154 155
155 156 if u1:
156 157 repo.ui.debug(_(" unmatched files in local:\n %s\n")
157 158 % "\n ".join(u1))
158 159 if u2:
159 160 repo.ui.debug(_(" unmatched files in other:\n %s\n")
160 161 % "\n ".join(u2))
161 162
162 163 for f in u1:
163 checkcopies(ctx(f, m1[f]), m2, ma)
164 checkcopies(f, m1, m2)
164 165
165 166 for f in u2:
166 checkcopies(ctx(f, m2[f]), m1, ma)
167 checkcopies(f, m2, m1)
167 168
168 169 diverge2 = {}
169 170 for of, fl in diverge.items():
170 171 if len(fl) == 1:
171 172 del diverge[of] # not actually divergent
172 173 else:
173 174 diverge2.update(dict.fromkeys(fl)) # reverse map for below
174 175
175 176 if fullcopy:
176 177 repo.ui.debug(_(" all copies found (* = to merge, ! = divergent):\n"))
177 178 for f in fullcopy:
178 179 note = ""
179 180 if f in copy: note += "*"
180 181 if f in diverge2: note += "!"
181 182 repo.ui.debug(_(" %s -> %s %s\n") % (f, fullcopy[f], note))
182 183
183 184 del diverge2
184 185
185 186 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
186 187 return copy, diverge
187 188
188 189 repo.ui.debug(_(" checking for directory renames\n"))
189 190
190 191 # generate a directory move map
191 192 d1, d2 = _dirs(m1), _dirs(m2)
192 193 invalid = {}
193 194 dirmove = {}
194 195
195 196 # examine each file copy for a potential directory move, which is
196 197 # when all the files in a directory are moved to a new directory
197 198 for dst, src in fullcopy.items():
198 199 dsrc, ddst = _dirname(src), _dirname(dst)
199 200 if dsrc in invalid:
200 201 # already seen to be uninteresting
201 202 continue
202 203 elif dsrc in d1 and ddst in d1:
203 204 # directory wasn't entirely moved locally
204 205 invalid[dsrc] = True
205 206 elif dsrc in d2 and ddst in d2:
206 207 # directory wasn't entirely moved remotely
207 208 invalid[dsrc] = True
208 209 elif dsrc in dirmove and dirmove[dsrc] != ddst:
209 210 # files from the same directory moved to two different places
210 211 invalid[dsrc] = True
211 212 else:
212 213 # looks good so far
213 214 dirmove[dsrc + "/"] = ddst + "/"
214 215
215 216 for i in invalid:
216 217 if i in dirmove:
217 218 del dirmove[i]
218 219
219 220 del d1, d2, invalid
220 221
221 222 if not dirmove:
222 223 return copy, diverge
223 224
224 225 for d in dirmove:
225 226 repo.ui.debug(_(" dir %s -> %s\n") % (d, dirmove[d]))
226 227
227 228 # check unaccounted nonoverlapping files against directory moves
228 229 for f in u1 + u2:
229 230 if f not in fullcopy:
230 231 for d in dirmove:
231 232 if f.startswith(d):
232 233 # new file added in a directory that was moved, move it
233 234 copy[f] = dirmove[d] + f[len(d):]
234 235 repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f]))
235 236 break
236 237
237 238 return copy, diverge
238 239
239 240 def _symmetricdifference(repo, rev1, rev2):
240 241 """symmetric difference of the sets of ancestors of rev1 and rev2
241 242
242 243 I.e. revisions that are ancestors of rev1 or rev2, but not both.
243 244 """
244 245 # basic idea:
245 246 # - mark rev1 and rev2 with different colors
246 247 # - walk the graph in topological order with the help of a heap;
247 248 # for each revision r:
248 249 # - if r has only one color, we want to return it
249 250 # - add colors[r] to its parents
250 251 #
251 252 # We keep track of the number of revisions in the heap that
252 253 # we may be interested in. We stop walking the graph as soon
253 254 # as this number reaches 0.
254 255 WHITE = 1
255 256 BLACK = 2
256 257 ALLCOLORS = WHITE | BLACK
257 258 colors = {rev1: WHITE, rev2: BLACK}
258 259
259 260 cl = repo.changelog
260 261
261 262 visit = [-rev1, -rev2]
262 263 heapq.heapify(visit)
263 264 n_wanted = len(visit)
264 265 ret = []
265 266
266 267 while n_wanted:
267 268 r = -heapq.heappop(visit)
268 269 wanted = colors[r] != ALLCOLORS
269 270 n_wanted -= wanted
270 271 if wanted:
271 272 ret.append(r)
272 273
273 274 for p in cl.parentrevs(r):
274 275 if p == nullrev:
275 276 continue
276 277 if p not in colors:
277 278 # first time we see p; add it to visit
278 279 n_wanted += wanted
279 280 colors[p] = colors[r]
280 281 heapq.heappush(visit, -p)
281 282 elif colors[p] != ALLCOLORS and colors[p] != colors[r]:
282 283 # at first we thought we wanted p, but now
283 284 # we know we don't really want it
284 285 n_wanted -= 1
285 286 colors[p] |= colors[r]
286 287
287 288 del colors[r]
288 289
289 290 return ret
290 291
291 292 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
292 293 """
293 294 Merge p1 and p2 with ancestor ma and generate merge action list
294 295
295 296 overwrite = whether we clobber working files
296 297 partial = function to filter file lists
297 298 """
298 299
299 300 repo.ui.note(_("resolving manifests\n"))
300 301 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
301 302 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
302 303
303 304 m1 = p1.manifest()
304 305 m2 = p2.manifest()
305 306 ma = pa.manifest()
306 307 backwards = (pa == p2)
307 308 action = []
308 309 copy = {}
309 310 diverge = {}
310 311
311 312 def fmerge(f, f2=None, fa=None):
312 313 """merge flags"""
313 314 if not f2:
314 315 f2 = f
315 316 fa = f
316 317 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
317 318 if m == n: # flags agree
318 319 return m # unchanged
319 320 if m and n: # flags are set but don't agree
320 321 if not a: # both differ from parent
321 322 r = repo.ui.prompt(
322 323 _(" conflicting flags for %s\n"
323 324 "(n)one, e(x)ec or sym(l)ink?") % f, "[nxl]", "n")
324 325 return r != "n" and r or ''
325 326 if m == a:
326 327 return n # changed from m to n
327 328 return m # changed from n to m
328 329 if m and m != a: # changed from a to m
329 330 return m
330 331 if n and n != a: # changed from a to n
331 332 return n
332 333 return '' # flag was cleared
333 334
334 335 def act(msg, m, f, *args):
335 336 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
336 337 action.append((f, m) + args)
337 338
338 339 if not (backwards or overwrite):
339 340 rev1 = p1.rev()
340 341 if rev1 is None:
341 342 # p1 is a workingctx
342 343 rev1 = p1.parents()[0].rev()
343 344 limit = min(_symmetricdifference(repo, rev1, p2.rev()))
344 345 copy, diverge = findcopies(repo, m1, m2, ma, limit)
345 346
346 347 for of, fl in diverge.items():
347 348 act("divergent renames", "dr", of, fl)
348 349
349 350 copied = dict.fromkeys(copy.values())
350 351
351 352 # Compare manifests
352 353 for f, n in m1.iteritems():
353 354 if partial and not partial(f):
354 355 continue
355 356 if f in m2:
356 357 if overwrite or backwards:
357 358 rflags = m2.flags(f)
358 359 else:
359 360 rflags = fmerge(f)
360 361 # are files different?
361 362 if n != m2[f]:
362 363 a = ma.get(f, nullid)
363 364 # are we clobbering?
364 365 if overwrite:
365 366 act("clobbering", "g", f, rflags)
366 367 # or are we going back in time and clean?
367 368 elif backwards and not n[20:]:
368 369 act("reverting", "g", f, rflags)
369 370 # are both different from the ancestor?
370 371 elif n != a and m2[f] != a:
371 372 act("versions differ", "m", f, f, f, rflags, False)
372 373 # is remote's version newer?
373 374 elif m2[f] != a:
374 375 act("remote is newer", "g", f, rflags)
375 376 # local is newer, not overwrite, check mode bits
376 377 elif m1.flags(f) != rflags:
377 378 act("update permissions", "e", f, rflags)
378 379 # contents same, check mode bits
379 380 elif m1.flags(f) != rflags:
380 381 act("update permissions", "e", f, rflags)
381 382 elif f in copied:
382 383 continue
383 384 elif f in copy:
384 385 f2 = copy[f]
385 386 if f2 not in m2: # directory rename
386 387 act("remote renamed directory to " + f2, "d",
387 388 f, None, f2, m1.flags(f))
388 389 elif f2 in m1: # case 2 A,B/B/B
389 390 act("local copied to " + f2, "m",
390 391 f, f2, f, fmerge(f, f2, f2), False)
391 392 else: # case 4,21 A/B/B
392 393 act("local moved to " + f2, "m",
393 394 f, f2, f, fmerge(f, f2, f2), False)
394 395 elif f in ma:
395 396 if n != ma[f] and not overwrite:
396 397 if repo.ui.prompt(
397 398 _(" local changed %s which remote deleted\n"
398 399 "use (c)hanged version or (d)elete?") % f,
399 400 _("[cd]"), _("c")) == _("d"):
400 401 act("prompt delete", "r", f)
401 402 else:
402 403 act("other deleted", "r", f)
403 404 else:
404 405 # file is created on branch or in working directory
405 406 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
406 407 act("remote deleted", "r", f)
407 408
408 409 for f, n in m2.iteritems():
409 410 if partial and not partial(f):
410 411 continue
411 412 if f in m1:
412 413 continue
413 414 if f in copied:
414 415 continue
415 416 if f in copy:
416 417 f2 = copy[f]
417 418 if f2 not in m1: # directory rename
418 419 act("local renamed directory to " + f2, "d",
419 420 None, f, f2, m2.flags(f))
420 421 elif f2 in m2: # rename case 1, A/A,B/A
421 422 act("remote copied to " + f, "m",
422 423 f2, f, f, fmerge(f2, f, f2), False)
423 424 else: # case 3,20 A/B/A
424 425 act("remote moved to " + f, "m",
425 426 f2, f, f, fmerge(f2, f, f2), True)
426 427 elif f in ma:
427 428 if overwrite or backwards:
428 429 act("recreating", "g", f, m2.flags(f))
429 430 elif n != ma[f]:
430 431 if repo.ui.prompt(
431 432 _("remote changed %s which local deleted\n"
432 433 "use (c)hanged version or leave (d)eleted?") % f,
433 434 _("[cd]"), _("c")) == _("c"):
434 435 act("prompt recreating", "g", f, m2.flags(f))
435 436 else:
436 437 act("remote created", "g", f, m2.flags(f))
437 438
438 439 return action
439 440
440 441 def applyupdates(repo, action, wctx, mctx):
441 442 "apply the merge action list to the working directory"
442 443
443 444 updated, merged, removed, unresolved = 0, 0, 0, 0
444 445 action.sort()
445 446 # prescan for copy/renames
446 447 for a in action:
447 448 f, m = a[:2]
448 449 if m == 'm': # merge
449 450 f2, fd, flags, move = a[2:]
450 451 if f != fd:
451 452 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
452 453 repo.wwrite(fd, repo.wread(f), flags)
453 454
454 455 audit_path = util.path_auditor(repo.root)
455 456
456 457 for a in action:
457 458 f, m = a[:2]
458 459 if f and f[0] == "/":
459 460 continue
460 461 if m == "r": # remove
461 462 repo.ui.note(_("removing %s\n") % f)
462 463 audit_path(f)
463 464 try:
464 465 util.unlink(repo.wjoin(f))
465 466 except OSError, inst:
466 467 if inst.errno != errno.ENOENT:
467 468 repo.ui.warn(_("update failed to remove %s: %s!\n") %
468 469 (f, inst.strerror))
469 470 removed += 1
470 471 elif m == "m": # merge
471 472 f2, fd, flags, move = a[2:]
472 473 r = filemerge.filemerge(repo, f, fd, f2, wctx, mctx)
473 474 if r > 0:
474 475 unresolved += 1
475 476 else:
476 477 if r is None:
477 478 updated += 1
478 479 else:
479 480 merged += 1
480 481 util.set_flags(repo.wjoin(fd), flags)
481 482 if f != fd and move and util.lexists(repo.wjoin(f)):
482 483 repo.ui.debug(_("removing %s\n") % f)
483 484 os.unlink(repo.wjoin(f))
484 485 elif m == "g": # get
485 486 flags = a[2]
486 487 repo.ui.note(_("getting %s\n") % f)
487 488 t = mctx.filectx(f).data()
488 489 repo.wwrite(f, t, flags)
489 490 updated += 1
490 491 elif m == "d": # directory rename
491 492 f2, fd, flags = a[2:]
492 493 if f:
493 494 repo.ui.note(_("moving %s to %s\n") % (f, fd))
494 495 t = wctx.filectx(f).data()
495 496 repo.wwrite(fd, t, flags)
496 497 util.unlink(repo.wjoin(f))
497 498 if f2:
498 499 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
499 500 t = mctx.filectx(f2).data()
500 501 repo.wwrite(fd, t, flags)
501 502 updated += 1
502 503 elif m == "dr": # divergent renames
503 504 fl = a[2]
504 505 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
505 506 for nf in fl:
506 507 repo.ui.warn(" %s\n" % nf)
507 508 elif m == "e": # exec
508 509 flags = a[2]
509 510 util.set_flags(repo.wjoin(f), flags)
510 511
511 512 return updated, merged, removed, unresolved
512 513
513 514 def recordupdates(repo, action, branchmerge):
514 515 "record merge actions to the dirstate"
515 516
516 517 for a in action:
517 518 f, m = a[:2]
518 519 if m == "r": # remove
519 520 if branchmerge:
520 521 repo.dirstate.remove(f)
521 522 else:
522 523 repo.dirstate.forget(f)
523 524 elif m == "f": # forget
524 525 repo.dirstate.forget(f)
525 526 elif m in "ge": # get or exec change
526 527 if branchmerge:
527 528 repo.dirstate.normaldirty(f)
528 529 else:
529 530 repo.dirstate.normal(f)
530 531 elif m == "m": # merge
531 532 f2, fd, flag, move = a[2:]
532 533 if branchmerge:
533 534 # We've done a branch merge, mark this file as merged
534 535 # so that we properly record the merger later
535 536 repo.dirstate.merge(fd)
536 537 if f != f2: # copy/rename
537 538 if move:
538 539 repo.dirstate.remove(f)
539 540 if f != fd:
540 541 repo.dirstate.copy(f, fd)
541 542 else:
542 543 repo.dirstate.copy(f2, fd)
543 544 else:
544 545 # We've update-merged a locally modified file, so
545 546 # we set the dirstate to emulate a normal checkout
546 547 # of that file some time in the past. Thus our
547 548 # merge will appear as a normal local file
548 549 # modification.
549 550 repo.dirstate.normallookup(fd)
550 551 if move:
551 552 repo.dirstate.forget(f)
552 553 elif m == "d": # directory rename
553 554 f2, fd, flag = a[2:]
554 555 if not f2 and f not in repo.dirstate:
555 556 # untracked file moved
556 557 continue
557 558 if branchmerge:
558 559 repo.dirstate.add(fd)
559 560 if f:
560 561 repo.dirstate.remove(f)
561 562 repo.dirstate.copy(f, fd)
562 563 if f2:
563 564 repo.dirstate.copy(f2, fd)
564 565 else:
565 566 repo.dirstate.normal(fd)
566 567 if f:
567 568 repo.dirstate.forget(f)
568 569
569 570 def update(repo, node, branchmerge, force, partial):
570 571 """
571 572 Perform a merge between the working directory and the given node
572 573
573 574 branchmerge = whether to merge between branches
574 575 force = whether to force branch merging or file overwriting
575 576 partial = a function to filter file lists (dirstate not updated)
576 577 """
577 578
578 579 wlock = repo.wlock()
579 580 try:
580 581 wc = repo.workingctx()
581 582 if node is None:
582 583 # tip of current branch
583 584 try:
584 585 node = repo.branchtags()[wc.branch()]
585 586 except KeyError:
586 587 if wc.branch() == "default": # no default branch!
587 588 node = repo.lookup("tip") # update to tip
588 589 else:
589 590 raise util.Abort(_("branch %s not found") % wc.branch())
590 591 overwrite = force and not branchmerge
591 592 forcemerge = force and branchmerge
592 593 pl = wc.parents()
593 594 p1, p2 = pl[0], repo.changectx(node)
594 595 pa = p1.ancestor(p2)
595 596 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
596 597 fastforward = False
597 598
598 599 ### check phase
599 600 if not overwrite and len(pl) > 1:
600 601 raise util.Abort(_("outstanding uncommitted merges"))
601 602 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
602 603 if branchmerge:
603 604 if p1.branch() != p2.branch() and pa != p2:
604 605 fastforward = True
605 606 else:
606 607 raise util.Abort(_("there is nothing to merge, just use "
607 608 "'hg update' or look at 'hg heads'"))
608 609 elif not (overwrite or branchmerge):
609 610 raise util.Abort(_("update spans branches, use 'hg merge' "
610 611 "or 'hg update -C' to lose changes"))
611 612 if branchmerge and not forcemerge:
612 613 if wc.files() or wc.deleted():
613 614 raise util.Abort(_("outstanding uncommitted changes"))
614 615
615 616 ### calculate phase
616 617 action = []
617 618 if not force:
618 619 _checkunknown(wc, p2)
619 620 if not util.checkfolding(repo.path):
620 621 _checkcollision(p2)
621 622 action += _forgetremoved(wc, p2, branchmerge)
622 623 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
623 624
624 625 ### apply phase
625 626 if not branchmerge: # just jump to the new rev
626 627 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
627 628 if not partial:
628 629 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
629 630
630 631 stats = applyupdates(repo, action, wc, p2)
631 632
632 633 if not partial:
633 634 recordupdates(repo, action, branchmerge)
634 635 repo.dirstate.setparents(fp1, fp2)
635 636 if not branchmerge and not fastforward:
636 637 repo.dirstate.setbranch(p2.branch())
637 638 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
638 639
639 640 return stats
640 641 finally:
641 642 del wlock
General Comments 0
You need to be logged in to leave comments. Login now