##// END OF EJS Templates
merge: finish removing nodes from action list
Matt Mackall -
r3306:5c980655 default
parent child Browse files
Show More
@@ -1,463 +1,463 b''
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006 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 *
9 9 from i18n import gettext as _
10 10 from demandload import *
11 11 demandload(globals(), "errno util os tempfile")
12 12
13 13 def filemerge(repo, fw, fo, fd, wctx, mctx, move):
14 14 """perform a 3-way merge in the working directory
15 15
16 16 fw = filename in the working directory and first parent
17 17 fo = filename in other parent
18 18 fd = destination filename
19 19 wctx, mctx = working and merge changecontexts
20 20 move = whether to move or copy the file to the destination
21 21
22 22 TODO:
23 23 if fw is copied in the working directory, we get confused
24 24 implement move and fd
25 25 """
26 26
27 27 def temp(prefix, ctx):
28 28 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
29 29 (fd, name) = tempfile.mkstemp(prefix=pre)
30 30 f = os.fdopen(fd, "wb")
31 31 repo.wwrite(ctx.path(), ctx.data(), f)
32 32 f.close()
33 33 return name
34 34
35 35 fcm = wctx.filectx(fw)
36 36 fco = mctx.filectx(fo)
37 37 fca = fcm.ancestor(fco)
38 38 if not fca:
39 39 fca = repo.filectx(fw, fileid=-1)
40 40 a = repo.wjoin(fw)
41 41 b = temp("base", fca)
42 42 c = temp("other", fco)
43 43
44 44 repo.ui.note(_("resolving %s\n") % fw)
45 45 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
46 46
47 47 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
48 48 or "hgmerge")
49 49 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
50 50 environ={'HG_FILE': fw,
51 51 'HG_MY_NODE': str(wctx.parents()[0]),
52 52 'HG_OTHER_NODE': str(mctx)})
53 53 if r:
54 54 repo.ui.warn(_("merging %s failed!\n") % fw)
55 55 else:
56 56 if fd != fw:
57 57 repo.ui.debug(_("copying %s to %s\n") % (fw, fd))
58 58 repo.wwrite(fd, repo.wread(fw))
59 59 if move:
60 60 repo.ui.debug(_("removing %s\n") % fw)
61 61 os.unlink(a)
62 62
63 63 os.unlink(b)
64 64 os.unlink(c)
65 65 return r
66 66
67 67 def checkunknown(repo, m2, wctx):
68 68 """
69 69 check for collisions between unknown files and files in m2
70 70 """
71 71 for f in wctx.unknown():
72 72 if f in m2:
73 73 if repo.file(f).cmp(m2[f], repo.wread(f)):
74 74 raise util.Abort(_("'%s' already exists in the working"
75 75 " dir and differs from remote") % f)
76 76
77 77 def forgetremoved(m2, wctx):
78 78 """
79 79 Forget removed files
80 80
81 81 If we're jumping between revisions (as opposed to merging), and if
82 82 neither the working directory nor the target rev has the file,
83 83 then we need to remove it from the dirstate, to prevent the
84 84 dirstate from listing the file when it is no longer in the
85 85 manifest.
86 86 """
87 87
88 88 action = []
89 89
90 90 for f in wctx.deleted() + wctx.removed():
91 91 if f not in m2:
92 92 action.append((f, "f"))
93 93
94 94 return action
95 95
96 96 def nonoverlap(d1, d2):
97 97 """
98 98 Return list of elements in d1 not in d2
99 99 """
100 100
101 101 l = []
102 102 for d in d1:
103 103 if d not in d2:
104 104 l.append(d)
105 105
106 106 l.sort()
107 107 return l
108 108
109 109 def findold(fctx, limit):
110 110 """
111 111 find files that path was copied from, back to linkrev limit
112 112 """
113 113
114 114 old = {}
115 115 orig = fctx.path()
116 116 visit = [fctx]
117 117 while visit:
118 118 fc = visit.pop()
119 119 if fc.rev() < limit:
120 120 continue
121 121 if fc.path() != orig and fc.path() not in old:
122 122 old[fc.path()] = 1
123 123 visit += fc.parents()
124 124
125 125 old = old.keys()
126 126 old.sort()
127 127 return old
128 128
129 129 def findcopies(repo, m1, m2, limit):
130 130 """
131 131 Find moves and copies between m1 and m2 back to limit linkrev
132 132 """
133 133
134 134 if not repo.ui.config("merge", "followcopies"):
135 135 return {}
136 136
137 137 # avoid silly behavior for update from empty dir
138 138 if not m1:
139 139 return {}
140 140
141 141 dcopies = repo.dirstate.copies()
142 142 copy = {}
143 143 match = {}
144 144 u1 = nonoverlap(m1, m2)
145 145 u2 = nonoverlap(m2, m1)
146 146 ctx = util.cachefunc(lambda f,n: repo.filectx(f, fileid=n[:20]))
147 147
148 148 def checkpair(c, f2, man):
149 149 ''' check if an apparent pair actually matches '''
150 150 c2 = ctx(f2, man[f2])
151 151 ca = c.ancestor(c2)
152 152 if ca and ca.path() == c.path() or ca.path() == c2.path():
153 153 copy[c.path()] = f2
154 154 copy[f2] = c.path()
155 155
156 156 for f in u1:
157 157 c = ctx(dcopies.get(f, f), m1[f])
158 158 for of in findold(c, limit):
159 159 if of in m2:
160 160 checkpair(c, of, m2)
161 161 else:
162 162 match.setdefault(of, []).append(f)
163 163
164 164 for f in u2:
165 165 c = ctx(f, m2[f])
166 166 for of in findold(c, limit):
167 167 if of in m1:
168 168 checkpair(c, of, m1)
169 169 elif of in match:
170 170 for mf in match[of]:
171 171 checkpair(c, mf, m1)
172 172
173 173 return copy
174 174
175 175 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
176 176 """
177 177 Merge manifest m1 with m2 using ancestor ma and generate merge action list
178 178 """
179 179
180 180 m1 = p1.manifest()
181 181 m2 = p2.manifest()
182 182 ma = pa.manifest()
183 183 backwards = (pa == p2)
184 184
185 185 def fmerge(f, f2=None, fa=None):
186 186 """merge executable flags"""
187 187 if not f2:
188 188 f2 = f
189 189 fa = f
190 190 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
191 191 return ((a^b) | (a^c)) ^ a
192 192
193 193 action = []
194 194
195 195 def act(msg, f, m, *args):
196 196 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
197 197 action.append((f, m) + args)
198 198
199 199 copy = {}
200 200 if not (backwards or overwrite):
201 201 copy = findcopies(repo, m1, m2, pa.rev())
202 202
203 203 # Compare manifests
204 204 for f, n in m1.iteritems():
205 205 if partial and not partial(f):
206 206 continue
207 207 if f in m2:
208 208 # are files different?
209 209 if n != m2[f]:
210 210 a = ma.get(f, nullid)
211 211 # are both different from the ancestor?
212 212 if not overwrite and n != a and m2[f] != a:
213 213 act("versions differ", f, "m", fmerge(f))
214 214 # are we clobbering?
215 215 # is remote's version newer?
216 216 # or are we going back in time and clean?
217 217 elif overwrite or m2[f] != a or (backwards and not n[20:]):
218 act("remote is newer", f, "g", m2.execf(f), m2[f])
218 act("remote is newer", f, "g", m2.execf(f))
219 219 # local is newer, not overwrite, check mode bits
220 220 elif fmerge(f) != m1.execf(f):
221 221 act("update permissions", f, "e", m2.execf(f))
222 222 # contents same, check mode bits
223 223 elif m1.execf(f) != m2.execf(f):
224 224 if overwrite or fmerge(f) != m1.execf(f):
225 225 act("update permissions", f, "e", m2.execf(f))
226 226 elif f in copy:
227 227 f2 = copy[f]
228 228 if f in ma: # case 3,20 A/B/A
229 229 act("remote moved",
230 230 f, "c", f2, f2, fmerge(f, f2, f), True)
231 231 else:
232 232 if f2 in m1: # case 2 A,B/B/B
233 233 act("local copied",
234 234 f, "c", f2, f, fmerge(f, f2, f2), False)
235 235 else: # case 4,21 A/B/B
236 236 act("local moved",
237 237 f, "c", f2, f, fmerge(f, f2, f2), False)
238 238 elif f in ma:
239 239 if n != ma[f] and not overwrite:
240 240 if repo.ui.prompt(
241 241 (_(" local changed %s which remote deleted\n") % f) +
242 242 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
243 243 act("prompt delete", f, "r")
244 244 else:
245 245 act("other deleted", f, "r")
246 246 else:
247 247 # file is created on branch or in working directory
248 248 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
249 249 act("remote deleted", f, "r")
250 250
251 251 for f, n in m2.iteritems():
252 252 if partial and not partial(f):
253 253 continue
254 254 if f in m1:
255 255 continue
256 256 if f in copy:
257 257 f2 = copy[f]
258 258 if f2 not in m2: # already seen
259 259 continue
260 260 # rename case 1, A/A,B/A
261 261 act("remote copied", f2, "c", f, f, fmerge(f2, f, f2), False)
262 262 elif f in ma:
263 263 if overwrite or backwards:
264 act("recreating", f, "g", m2.execf(f), n)
264 act("recreating", f, "g", m2.execf(f))
265 265 elif n != ma[f]:
266 266 if repo.ui.prompt(
267 267 (_("remote changed %s which local deleted\n") % f) +
268 268 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
269 act("prompt recreating", f, "g", m2.execf(f), n)
269 act("prompt recreating", f, "g", m2.execf(f))
270 270 else:
271 act("remote created", f, "g", m2.execf(f), n)
271 act("remote created", f, "g", m2.execf(f))
272 272
273 273 return action
274 274
275 275 def applyupdates(repo, action, wctx, mctx):
276 276 updated, merged, removed, unresolved = 0, 0, 0, 0
277 277 action.sort()
278 278 for a in action:
279 279 f, m = a[:2]
280 280 if f[0] == "/":
281 281 continue
282 282 if m == "r": # remove
283 283 repo.ui.note(_("removing %s\n") % f)
284 284 util.audit_path(f)
285 285 try:
286 286 util.unlink(repo.wjoin(f))
287 287 except OSError, inst:
288 288 if inst.errno != errno.ENOENT:
289 289 repo.ui.warn(_("update failed to remove %s: %s!\n") %
290 290 (f, inst.strerror))
291 291 removed +=1
292 292 elif m == "c": # copy
293 293 f2, fd, flag, move = a[2:]
294 294 repo.ui.status(_("merging %s and %s to %s\n") % (f, f2, fd))
295 295 if filemerge(repo, f, f2, fd, wctx, mctx, move):
296 296 unresolved += 1
297 297 util.set_exec(repo.wjoin(fd), flag)
298 298 merged += 1
299 299 elif m == "m": # merge
300 300 flag = a[2]
301 301 repo.ui.status(_("merging %s\n") % f)
302 302 if filemerge(repo, f, f, f, wctx, mctx, False):
303 303 unresolved += 1
304 304 util.set_exec(repo.wjoin(f), flag)
305 305 merged += 1
306 306 elif m == "g": # get
307 flag, node = a[2:]
307 flag = a[2]
308 308 repo.ui.note(_("getting %s\n") % f)
309 309 t = mctx.filectx(f).data()
310 310 repo.wwrite(f, t)
311 311 util.set_exec(repo.wjoin(f), flag)
312 312 updated += 1
313 313 elif m == "e": # exec
314 314 flag = a[2]
315 315 util.set_exec(repo.wjoin(f), flag)
316 316
317 317 return updated, merged, removed, unresolved
318 318
319 319 def recordupdates(repo, action, branchmerge, mctx):
320 320 for a in action:
321 321 f, m = a[:2]
322 322 if m == "r": # remove
323 323 if branchmerge:
324 324 repo.dirstate.update([f], 'r')
325 325 else:
326 326 repo.dirstate.forget([f])
327 327 elif m == "f": # forget
328 328 repo.dirstate.forget([f])
329 329 elif m == "g": # get
330 330 if branchmerge:
331 331 repo.dirstate.update([f], 'n', st_mtime=-1)
332 332 else:
333 333 repo.dirstate.update([f], 'n')
334 334 elif m == "m": # merge
335 335 flag = a[2]
336 336 if branchmerge:
337 337 # We've done a branch merge, mark this file as merged
338 338 # so that we properly record the merger later
339 339 repo.dirstate.update([f], 'm')
340 340 else:
341 341 # We've update-merged a locally modified file, so
342 342 # we set the dirstate to emulate a normal checkout
343 343 # of that file some time in the past. Thus our
344 344 # merge will appear as a normal local file
345 345 # modification.
346 346 fl = repo.file(f)
347 347 f_len = mctx.filectx(f).size()
348 348 repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
349 349 elif m == "c": # copy
350 350 f2, fd, flag, move = a[2:]
351 351 if branchmerge:
352 352 # We've done a branch merge, mark this file as merged
353 353 # so that we properly record the merger later
354 354 repo.dirstate.update([fd], 'm')
355 355 else:
356 356 # We've update-merged a locally modified file, so
357 357 # we set the dirstate to emulate a normal checkout
358 358 # of that file some time in the past. Thus our
359 359 # merge will appear as a normal local file
360 360 # modification.
361 361 fl = repo.file(f)
362 362 f_len = mctx.filectx(f).size()
363 363 repo.dirstate.update([fd], 'n', st_size=f_len, st_mtime=-1)
364 364 if move:
365 365 repo.dirstate.update([f], 'r')
366 366 if f != fd:
367 367 repo.dirstate.copy(f, fd)
368 368 else:
369 369 repo.dirstate.copy(f2, fd)
370 370
371 371 def update(repo, node, branchmerge=False, force=False, partial=None,
372 372 wlock=None, show_stats=True, remind=True):
373 373
374 374 overwrite = force and not branchmerge
375 375 forcemerge = force and branchmerge
376 376
377 377 if not wlock:
378 378 wlock = repo.wlock()
379 379
380 380 ### check phase
381 381
382 382 wc = repo.workingctx()
383 383 pl = wc.parents()
384 384 if not overwrite and len(pl) > 1:
385 385 raise util.Abort(_("outstanding uncommitted merges"))
386 386
387 387 p1, p2 = pl[0], repo.changectx(node)
388 388 pa = p1.ancestor(p2)
389 389
390 390 # is there a linear path from p1 to p2?
391 391 if pa == p1 or pa == p2:
392 392 if branchmerge:
393 393 raise util.Abort(_("there is nothing to merge, just use "
394 394 "'hg update' or look at 'hg heads'"))
395 395 elif not (overwrite or branchmerge):
396 396 raise util.Abort(_("update spans branches, use 'hg merge' "
397 397 "or 'hg update -C' to lose changes"))
398 398
399 399 if branchmerge and not forcemerge:
400 400 if wc.modified() or wc.added() or wc.removed():
401 401 raise util.Abort(_("outstanding uncommitted changes"))
402 402
403 403 m1 = wc.manifest()
404 404 m2 = p2.manifest()
405 405
406 406 # resolve the manifest to determine which files
407 407 # we care about merging
408 408 repo.ui.note(_("resolving manifests\n"))
409 409 repo.ui.debug(_(" overwrite %s branchmerge %s partial %s\n") %
410 410 (overwrite, branchmerge, bool(partial)))
411 411 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (p1, p2, pa))
412 412
413 413 action = []
414 414
415 415 if not force:
416 416 checkunknown(repo, m2, wc)
417 417 if not branchmerge:
418 418 action += forgetremoved(m2, wc)
419 419
420 420 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
421 421
422 422 ### apply phase
423 423
424 424 if not branchmerge:
425 425 # just jump to the new rev
426 426 fp1, fp2, xp1, xp2 = p2.node(), nullid, str(p2), ''
427 427 else:
428 428 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
429 429
430 430 if not partial:
431 431 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
432 432
433 433 updated, merged, removed, unresolved = applyupdates(repo, action, wc, p2)
434 434
435 435 # update dirstate
436 436 if not partial:
437 437 recordupdates(repo, action, branchmerge, p2)
438 438 repo.dirstate.setparents(fp1, fp2)
439 439 repo.hook('update', parent1=xp1, parent2=xp2, error=unresolved)
440 440
441 441 if show_stats:
442 442 stats = ((updated, _("updated")),
443 443 (merged - unresolved, _("merged")),
444 444 (removed, _("removed")),
445 445 (unresolved, _("unresolved")))
446 446 note = ", ".join([_("%d files %s") % s for s in stats])
447 447 repo.ui.status("%s\n" % note)
448 448 if not partial:
449 449 if branchmerge:
450 450 if unresolved:
451 451 repo.ui.status(_("There are unresolved merges,"
452 452 " you can redo the full merge using:\n"
453 453 " hg update -C %s\n"
454 454 " hg merge %s\n"
455 455 % (p1.rev(), p2.rev())))
456 456 elif remind:
457 457 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
458 458 elif unresolved:
459 459 repo.ui.status(_("There are unresolved merges with"
460 460 " locally modified files.\n"))
461 461
462 462 return unresolved
463 463
General Comments 0
You need to be logged in to leave comments. Login now