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