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