##// END OF EJS Templates
merge: remove a flags case
Matt Mackall -
r8735:ff8519c4 default
parent child Browse files
Show More
@@ -1,501 +1,498 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 if overwrite or backwards:
178 rflags = m2.flags(f)
179 else:
180 177 rflags = fmerge(f, f, f)
181 178 # are files different?
182 179 if n != m2[f]:
183 180 a = ma.get(f, nullid)
184 181 # are we clobbering?
185 182 if overwrite:
186 183 act("clobbering", "g", f, rflags)
187 184 # or are we going back in time and clean?
188 185 elif backwards:
189 186 if not n[20:] or not p2[f].cmp(p1[f].data()):
190 187 act("reverting", "g", f, rflags)
191 188 # are both different from the ancestor?
192 189 elif n != a and m2[f] != a:
193 190 act("versions differ", "m", f, f, f, rflags, False)
194 191 # is remote's version newer?
195 192 elif m2[f] != a:
196 193 act("remote is newer", "g", f, rflags)
197 194 # local is newer, not overwrite, check mode bits
198 195 elif m1.flags(f) != rflags:
199 196 act("update permissions", "e", f, rflags)
200 197 # contents same, check mode bits
201 198 elif m1.flags(f) != rflags:
202 199 act("update permissions", "e", f, rflags)
203 200 elif f in copied:
204 201 continue
205 202 elif f in copy:
206 203 f2 = copy[f]
207 204 if f2 not in m2: # directory rename
208 205 act("remote renamed directory to " + f2, "d",
209 206 f, None, f2, m1.flags(f))
210 207 elif f2 in m1: # case 2 A,B/B/B
211 208 act("local copied to " + f2, "m",
212 209 f, f2, f, fmerge(f, f2, f2), False)
213 210 else: # case 4,21 A/B/B
214 211 act("local moved to " + f2, "m",
215 212 f, f2, f, fmerge(f, f2, f2), False)
216 213 elif f in ma and not n[20:]:
217 214 if n != ma[f] and not overwrite:
218 215 if repo.ui.prompt(
219 216 _(" local changed %s which remote deleted\n"
220 217 "use (c)hanged version or (d)elete?") % f,
221 218 (_("&Changed"), _("&Delete")), _("c")) == _("d"):
222 219 act("prompt delete", "r", f)
223 220 act("prompt keep", "a", f)
224 221 else:
225 222 act("other deleted", "r", f)
226 223 elif overwrite and n[20:] == "a": # do not erase the working copy
227 224 act("remote deleted", "f", f)
228 225 else:
229 226 # file is created on branch or in working directory
230 227 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
231 228 act("remote deleted", "r", f)
232 229
233 230 for f, n in m2.iteritems():
234 231 if partial and not partial(f):
235 232 continue
236 233 if f in m1:
237 234 continue
238 235 if f in copied:
239 236 continue
240 237 if f in copy:
241 238 f2 = copy[f]
242 239 if f2 not in m1: # directory rename
243 240 act("local renamed directory to " + f2, "d",
244 241 None, f, f2, m2.flags(f))
245 242 elif f2 in m2: # rename case 1, A/A,B/A
246 243 act("remote copied to " + f, "m",
247 244 f2, f, f, fmerge(f2, f, f2), False)
248 245 else: # case 3,20 A/B/A
249 246 act("remote moved to " + f, "m",
250 247 f2, f, f, fmerge(f2, f, f2), True)
251 248 elif f in ma:
252 249 if overwrite or backwards:
253 250 act("recreating", "g", f, m2.flags(f))
254 251 elif n != ma[f]:
255 252 if repo.ui.prompt(
256 253 _("remote changed %s which local deleted\n"
257 254 "use (c)hanged version or leave (d)eleted?") % f,
258 255 (_("&Changed"), _("&Deleted")), _("c")) == _("c"):
259 256 act("prompt recreating", "g", f, m2.flags(f))
260 257 else:
261 258 act("remote created", "g", f, m2.flags(f))
262 259
263 260 return action
264 261
265 262 def actionkey(a):
266 263 return a[1] == 'r' and -1 or 0, a
267 264
268 265 def applyupdates(repo, action, wctx, mctx):
269 266 "apply the merge action list to the working directory"
270 267
271 268 updated, merged, removed, unresolved = 0, 0, 0, 0
272 269 ms = mergestate(repo)
273 270 ms.reset(wctx.parents()[0].node())
274 271 moves = []
275 272 action.sort(key=actionkey)
276 273
277 274 # prescan for merges
278 275 for a in action:
279 276 f, m = a[:2]
280 277 if m == 'm': # merge
281 278 f2, fd, flags, move = a[2:]
282 279 repo.ui.debug(_("preserving %s for resolve of %s\n") % (f, fd))
283 280 fcl = wctx[f]
284 281 fco = mctx[f2]
285 282 fca = fcl.ancestor(fco) or repo.filectx(f, fileid=nullrev)
286 283 ms.add(fcl, fco, fca, fd, flags)
287 284 if f != fd and move:
288 285 moves.append(f)
289 286
290 287 # remove renamed files after safely stored
291 288 for f in moves:
292 289 if util.lexists(repo.wjoin(f)):
293 290 repo.ui.debug(_("removing %s\n") % f)
294 291 os.unlink(repo.wjoin(f))
295 292
296 293 audit_path = util.path_auditor(repo.root)
297 294
298 295 for a in action:
299 296 f, m = a[:2]
300 297 if f and f[0] == "/":
301 298 continue
302 299 if m == "r": # remove
303 300 repo.ui.note(_("removing %s\n") % f)
304 301 audit_path(f)
305 302 try:
306 303 util.unlink(repo.wjoin(f))
307 304 except OSError, inst:
308 305 if inst.errno != errno.ENOENT:
309 306 repo.ui.warn(_("update failed to remove %s: %s!\n") %
310 307 (f, inst.strerror))
311 308 removed += 1
312 309 elif m == "m": # merge
313 310 f2, fd, flags, move = a[2:]
314 311 r = ms.resolve(fd, wctx, mctx)
315 312 if r > 0:
316 313 unresolved += 1
317 314 else:
318 315 if r is None:
319 316 updated += 1
320 317 else:
321 318 merged += 1
322 319 util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
323 320 if f != fd and move and util.lexists(repo.wjoin(f)):
324 321 repo.ui.debug(_("removing %s\n") % f)
325 322 os.unlink(repo.wjoin(f))
326 323 elif m == "g": # get
327 324 flags = a[2]
328 325 repo.ui.note(_("getting %s\n") % f)
329 326 t = mctx.filectx(f).data()
330 327 repo.wwrite(f, t, flags)
331 328 updated += 1
332 329 elif m == "d": # directory rename
333 330 f2, fd, flags = a[2:]
334 331 if f:
335 332 repo.ui.note(_("moving %s to %s\n") % (f, fd))
336 333 t = wctx.filectx(f).data()
337 334 repo.wwrite(fd, t, flags)
338 335 util.unlink(repo.wjoin(f))
339 336 if f2:
340 337 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
341 338 t = mctx.filectx(f2).data()
342 339 repo.wwrite(fd, t, flags)
343 340 updated += 1
344 341 elif m == "dr": # divergent renames
345 342 fl = a[2]
346 343 repo.ui.warn(_("warning: detected divergent renames of %s to:\n") % f)
347 344 for nf in fl:
348 345 repo.ui.warn(" %s\n" % nf)
349 346 elif m == "e": # exec
350 347 flags = a[2]
351 348 util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags)
352 349
353 350 return updated, merged, removed, unresolved
354 351
355 352 def recordupdates(repo, action, branchmerge):
356 353 "record merge actions to the dirstate"
357 354
358 355 for a in action:
359 356 f, m = a[:2]
360 357 if m == "r": # remove
361 358 if branchmerge:
362 359 repo.dirstate.remove(f)
363 360 else:
364 361 repo.dirstate.forget(f)
365 362 elif m == "a": # re-add
366 363 if not branchmerge:
367 364 repo.dirstate.add(f)
368 365 elif m == "f": # forget
369 366 repo.dirstate.forget(f)
370 367 elif m == "e": # exec change
371 368 repo.dirstate.normallookup(f)
372 369 elif m == "g": # get
373 370 if branchmerge:
374 371 repo.dirstate.normaldirty(f)
375 372 else:
376 373 repo.dirstate.normal(f)
377 374 elif m == "m": # merge
378 375 f2, fd, flag, move = a[2:]
379 376 if branchmerge:
380 377 # We've done a branch merge, mark this file as merged
381 378 # so that we properly record the merger later
382 379 repo.dirstate.merge(fd)
383 380 if f != f2: # copy/rename
384 381 if move:
385 382 repo.dirstate.remove(f)
386 383 if f != fd:
387 384 repo.dirstate.copy(f, fd)
388 385 else:
389 386 repo.dirstate.copy(f2, fd)
390 387 else:
391 388 # We've update-merged a locally modified file, so
392 389 # we set the dirstate to emulate a normal checkout
393 390 # of that file some time in the past. Thus our
394 391 # merge will appear as a normal local file
395 392 # modification.
396 393 repo.dirstate.normallookup(fd)
397 394 if move:
398 395 repo.dirstate.forget(f)
399 396 elif m == "d": # directory rename
400 397 f2, fd, flag = a[2:]
401 398 if not f2 and f not in repo.dirstate:
402 399 # untracked file moved
403 400 continue
404 401 if branchmerge:
405 402 repo.dirstate.add(fd)
406 403 if f:
407 404 repo.dirstate.remove(f)
408 405 repo.dirstate.copy(f, fd)
409 406 if f2:
410 407 repo.dirstate.copy(f2, fd)
411 408 else:
412 409 repo.dirstate.normal(fd)
413 410 if f:
414 411 repo.dirstate.forget(f)
415 412
416 413 def update(repo, node, branchmerge, force, partial):
417 414 """
418 415 Perform a merge between the working directory and the given node
419 416
420 417 branchmerge = whether to merge between branches
421 418 force = whether to force branch merging or file overwriting
422 419 partial = a function to filter file lists (dirstate not updated)
423 420 """
424 421
425 422 wlock = repo.wlock()
426 423 try:
427 424 wc = repo[None]
428 425 if node is None:
429 426 # tip of current branch
430 427 try:
431 428 node = repo.branchtags()[wc.branch()]
432 429 except KeyError:
433 430 if wc.branch() == "default": # no default branch!
434 431 node = repo.lookup("tip") # update to tip
435 432 else:
436 433 raise util.Abort(_("branch %s not found") % wc.branch())
437 434 overwrite = force and not branchmerge
438 435 pl = wc.parents()
439 436 p1, p2 = pl[0], repo[node]
440 437 pa = p1.ancestor(p2)
441 438 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
442 439 fastforward = False
443 440
444 441 ### check phase
445 442 if not overwrite and len(pl) > 1:
446 443 raise util.Abort(_("outstanding uncommitted merges"))
447 444 if branchmerge:
448 445 if pa == p2:
449 446 raise util.Abort(_("can't merge with ancestor"))
450 447 elif pa == p1:
451 448 if p1.branch() != p2.branch():
452 449 fastforward = True
453 450 else:
454 451 raise util.Abort(_("nothing to merge (use 'hg update'"
455 452 " or check 'hg heads')"))
456 453 if not force and (wc.files() or wc.deleted()):
457 454 raise util.Abort(_("outstanding uncommitted changes "
458 455 "(use 'hg status' to list changes)"))
459 456 elif not overwrite:
460 457 if pa == p1 or pa == p2: # linear
461 458 pass # all good
462 459 elif p1.branch() == p2.branch():
463 460 if wc.files() or wc.deleted():
464 461 raise util.Abort(_("crosses branches (use 'hg merge' or "
465 462 "'hg update -C' to discard changes)"))
466 463 raise util.Abort(_("crosses branches (use 'hg merge' "
467 464 "or 'hg update -C')"))
468 465 elif wc.files() or wc.deleted():
469 466 raise util.Abort(_("crosses named branches (use "
470 467 "'hg update -C' to discard changes)"))
471 468 else:
472 469 # Allow jumping branches if there are no changes
473 470 overwrite = True
474 471
475 472 ### calculate phase
476 473 action = []
477 474 if not force:
478 475 _checkunknown(wc, p2)
479 476 if not util.checkcase(repo.path):
480 477 _checkcollision(p2)
481 478 action += _forgetremoved(wc, p2, branchmerge)
482 479 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
483 480
484 481 ### apply phase
485 482 if not branchmerge: # just jump to the new rev
486 483 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
487 484 if not partial:
488 485 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
489 486
490 487 stats = applyupdates(repo, action, wc, p2)
491 488
492 489 if not partial:
493 490 recordupdates(repo, action, branchmerge)
494 491 repo.dirstate.setparents(fp1, fp2)
495 492 if not branchmerge and not fastforward:
496 493 repo.dirstate.setbranch(p2.branch())
497 494 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
498 495
499 496 return stats
500 497 finally:
501 498 wlock.release()
General Comments 0
You need to be logged in to leave comments. Login now