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