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