##// END OF EJS Templates
merge: simplify a check in checkcopies
Matt Mackall -
r4399:93652499 default
parent child Browse files
Show More
@@ -1,524 +1,524 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, wctx, mctx):
14 14 """perform a 3-way merge in the working directory
15 15
16 16 fw = filename in the working directory
17 17 fo = filename in other parent
18 18 wctx, mctx = working and merge changecontexts
19 19 """
20 20
21 21 def temp(prefix, ctx):
22 22 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
23 23 (fd, name) = tempfile.mkstemp(prefix=pre)
24 24 f = os.fdopen(fd, "wb")
25 25 repo.wwrite(ctx.path(), ctx.data(), f)
26 26 f.close()
27 27 return name
28 28
29 29 fcm = wctx.filectx(fw)
30 30 fco = mctx.filectx(fo)
31 31
32 32 if not fco.cmp(fcm.data()): # files identical?
33 33 return None
34 34
35 35 fca = fcm.ancestor(fco)
36 36 if not fca:
37 37 fca = repo.filectx(fw, fileid=nullrev)
38 38 a = repo.wjoin(fw)
39 39 b = temp("base", fca)
40 40 c = temp("other", fco)
41 41
42 42 if fw != fo:
43 43 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
44 44 else:
45 45 repo.ui.status(_("merging %s\n") % fw)
46 46
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': str(wctx.parents()[0]),
54 54 'HG_OTHER_NODE': str(mctx)})
55 55 if r:
56 56 repo.ui.warn(_("merging %s failed!\n") % fw)
57 57
58 58 os.unlink(b)
59 59 os.unlink(c)
60 60 return r
61 61
62 62 def checkunknown(wctx, mctx):
63 63 "check for collisions between unknown files and files in mctx"
64 64 man = mctx.manifest()
65 65 for f in wctx.unknown():
66 66 if f in man:
67 67 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
68 68 raise util.Abort(_("untracked local file '%s' differs"\
69 69 " from remote version") % f)
70 70
71 71 def checkcollision(mctx):
72 72 "check for case folding collisions in the destination context"
73 73 folded = {}
74 74 for fn in mctx.manifest():
75 75 fold = fn.lower()
76 76 if fold in folded:
77 77 raise util.Abort(_("case-folding collision between %s and %s")
78 78 % (fn, folded[fold]))
79 79 folded[fold] = fn
80 80
81 81 def forgetremoved(wctx, mctx):
82 82 """
83 83 Forget removed files
84 84
85 85 If we're jumping between revisions (as opposed to merging), and if
86 86 neither the working directory nor the target rev has the file,
87 87 then we need to remove it from the dirstate, to prevent the
88 88 dirstate from listing the file when it is no longer in the
89 89 manifest.
90 90 """
91 91
92 92 action = []
93 93 man = mctx.manifest()
94 94 for f in wctx.deleted() + wctx.removed():
95 95 if f not in man:
96 96 action.append((f, "f"))
97 97
98 98 return action
99 99
100 100 def findcopies(repo, m1, m2, ma, limit):
101 101 """
102 102 Find moves and copies between m1 and m2 back to limit linkrev
103 103 """
104 104
105 105 def dirname(f):
106 106 s = f.rfind("/")
107 107 if s == -1:
108 108 return ""
109 109 return f[:s]
110 110
111 111 def dirs(files):
112 112 d = {}
113 113 for f in files:
114 114 f = dirname(f)
115 115 while f not in d:
116 116 d[f] = True
117 117 f = dirname(f)
118 118 return d
119 119
120 120 def findold(fctx):
121 121 "find files that path was copied from, back to linkrev limit"
122 122 old = {}
123 123 seen = {}
124 124 orig = fctx.path()
125 125 visit = [fctx]
126 126 while visit:
127 127 fc = visit.pop()
128 128 s = str(fc)
129 129 if s in seen:
130 130 continue
131 131 seen[s] = 1
132 132 if fc.path() != orig and fc.path() not in old:
133 133 old[fc.path()] = 1
134 134 if fc.rev() < limit:
135 135 continue
136 136 visit += fc.parents()
137 137
138 138 old = old.keys()
139 139 old.sort()
140 140 return old
141 141
142 142 def nonoverlap(d1, d2, d3):
143 143 "Return list of elements in d1 not in d2 or d3"
144 144 l = [d for d in d1 if d not in d3 and d not in d2]
145 145 l.sort()
146 146 return l
147 147
148 148 def checkcopies(c, man):
149 149 '''check possible copies for filectx c'''
150 150 for of in findold(c):
151 151 if of not in man: # original file not in other manifest?
152 152 continue
153 153 c2 = ctx(of, man[of])
154 154 ca = c.ancestor(c2)
155 155 if not ca: # unrelated?
156 156 continue
157 157 # named changed on only one side?
158 158 if ca.path() == c.path() or ca.path() == c2.path():
159 159 fullcopy[c.path()] = of # remember for dir rename detection
160 if c == ca and c2 == ca: # no merge needed, ignore copy
160 if c == c2: # no merge needed, ignore copy
161 161 continue
162 162 copy[c.path()] = of
163 163
164 164 if not repo.ui.configbool("merge", "followcopies", True):
165 165 return {}
166 166
167 167 # avoid silly behavior for update from empty dir
168 168 if not m1 or not m2 or not ma:
169 169 return {}
170 170
171 171 dcopies = repo.dirstate.copies()
172 172 copy = {}
173 173 fullcopy = {}
174 174 u1 = nonoverlap(m1, m2, ma)
175 175 u2 = nonoverlap(m2, m1, ma)
176 176 ctx = util.cachefunc(lambda f, n: repo.filectx(f, fileid=n[:20]))
177 177
178 178 for f in u1:
179 179 checkcopies(ctx(dcopies.get(f, f), m1[f]), m2)
180 180
181 181 for f in u2:
182 182 checkcopies(ctx(f, m2[f]), m1)
183 183
184 184 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
185 185 return copy
186 186
187 187 # generate a directory move map
188 188 d1, d2 = dirs(m1), dirs(m2)
189 189 invalid = {}
190 190 dirmove = {}
191 191
192 192 # examine each file copy for a potential directory move, which is
193 193 # when all the files in a directory are moved to a new directory
194 194 for dst, src in fullcopy.items():
195 195 dsrc, ddst = dirname(src), dirname(dst)
196 196 if dsrc in invalid:
197 197 # already seen to be uninteresting
198 198 continue
199 199 elif dsrc in d1 and ddst in d1:
200 200 # directory wasn't entirely moved locally
201 201 invalid[dsrc] = True
202 202 elif dsrc in d2 and ddst in d2:
203 203 # directory wasn't entirely moved remotely
204 204 invalid[dsrc] = True
205 205 elif dsrc in dirmove and dirmove[dsrc] != ddst:
206 206 # files from the same directory moved to two different places
207 207 invalid[dsrc] = True
208 208 else:
209 209 # looks good so far
210 210 dirmove[dsrc + "/"] = ddst + "/"
211 211
212 212 for i in invalid:
213 213 if i in dirmove:
214 214 del dirmove[i]
215 215
216 216 del d1, d2, invalid
217 217
218 218 if not dirmove:
219 219 return copy
220 220
221 221 # check unaccounted nonoverlapping files against directory moves
222 222 for f in u1 + u2:
223 223 if f not in fullcopy:
224 224 for d in dirmove:
225 225 if f.startswith(d):
226 226 # new file added in a directory that was moved, move it
227 227 copy[f] = dirmove[d] + f[len(d):]
228 228 break
229 229
230 230 return copy
231 231
232 232 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
233 233 """
234 234 Merge p1 and p2 with ancestor ma and generate merge action list
235 235
236 236 overwrite = whether we clobber working files
237 237 partial = function to filter file lists
238 238 """
239 239
240 240 repo.ui.note(_("resolving manifests\n"))
241 241 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
242 242 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
243 243
244 244 m1 = p1.manifest()
245 245 m2 = p2.manifest()
246 246 ma = pa.manifest()
247 247 backwards = (pa == p2)
248 248 action = []
249 249 copy = {}
250 250
251 251 def fmerge(f, f2=None, fa=None):
252 252 """merge executable flags"""
253 253 if not f2:
254 254 f2 = f
255 255 fa = f
256 256 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
257 257 return ((a^b) | (a^c)) ^ a
258 258
259 259 def act(msg, m, f, *args):
260 260 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
261 261 action.append((f, m) + args)
262 262
263 263 if not (backwards or overwrite):
264 264 copy = findcopies(repo, m1, m2, ma, pa.rev())
265 265 copied = dict.fromkeys(copy.values())
266 266
267 267 # Compare manifests
268 268 for f, n in m1.iteritems():
269 269 if partial and not partial(f):
270 270 continue
271 271 if f in m2:
272 272 # are files different?
273 273 if n != m2[f]:
274 274 a = ma.get(f, nullid)
275 275 # are both different from the ancestor?
276 276 if not overwrite and n != a and m2[f] != a:
277 277 act("versions differ", "m", f, f, f, fmerge(f), False)
278 278 # are we clobbering?
279 279 # is remote's version newer?
280 280 # or are we going back in time and clean?
281 281 elif overwrite or m2[f] != a or (backwards and not n[20:]):
282 282 act("remote is newer", "g", f, m2.execf(f))
283 283 # local is newer, not overwrite, check mode bits
284 284 elif fmerge(f) != m1.execf(f):
285 285 act("update permissions", "e", f, m2.execf(f))
286 286 # contents same, check mode bits
287 287 elif m1.execf(f) != m2.execf(f):
288 288 if overwrite or fmerge(f) != m1.execf(f):
289 289 act("update permissions", "e", f, m2.execf(f))
290 290 elif f in copied:
291 291 continue
292 292 elif f in copy:
293 293 f2 = copy[f]
294 294 if f2 not in m2: # directory rename
295 295 act("remote renamed directory to " + f2, "d",
296 296 f, None, f2, m1.execf(f))
297 297 elif f2 in m1: # case 2 A,B/B/B
298 298 act("local copied to " + f2, "m",
299 299 f, f2, f, fmerge(f, f2, f2), False)
300 300 else: # case 4,21 A/B/B
301 301 act("local moved to " + f2, "m",
302 302 f, f2, f, fmerge(f, f2, f2), False)
303 303 elif f in ma:
304 304 if n != ma[f] and not overwrite:
305 305 if repo.ui.prompt(
306 306 (_(" local changed %s which remote deleted\n") % f) +
307 307 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
308 308 act("prompt delete", "r", f)
309 309 else:
310 310 act("other deleted", "r", f)
311 311 else:
312 312 # file is created on branch or in working directory
313 313 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
314 314 act("remote deleted", "r", f)
315 315
316 316 for f, n in m2.iteritems():
317 317 if partial and not partial(f):
318 318 continue
319 319 if f in m1:
320 320 continue
321 321 if f in copied:
322 322 continue
323 323 if f in copy:
324 324 f2 = copy[f]
325 325 if f2 not in m1: # directory rename
326 326 act("local renamed directory to " + f2, "d",
327 327 None, f, f2, m2.execf(f))
328 328 elif f2 in m2: # rename case 1, A/A,B/A
329 329 act("remote copied to " + f, "m",
330 330 f2, f, f, fmerge(f2, f, f2), False)
331 331 else: # case 3,20 A/B/A
332 332 act("remote moved to " + f, "m",
333 333 f2, f, f, fmerge(f2, f, f2), True)
334 334 elif f in ma:
335 335 if overwrite or backwards:
336 336 act("recreating", "g", f, m2.execf(f))
337 337 elif n != ma[f]:
338 338 if repo.ui.prompt(
339 339 (_("remote changed %s which local deleted\n") % f) +
340 340 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
341 341 act("prompt recreating", "g", f, m2.execf(f))
342 342 else:
343 343 act("remote created", "g", f, m2.execf(f))
344 344
345 345 return action
346 346
347 347 def applyupdates(repo, action, wctx, mctx):
348 348 "apply the merge action list to the working directory"
349 349
350 350 updated, merged, removed, unresolved = 0, 0, 0, 0
351 351 action.sort()
352 352 for a in action:
353 353 f, m = a[:2]
354 354 if f and f[0] == "/":
355 355 continue
356 356 if m == "r": # remove
357 357 repo.ui.note(_("removing %s\n") % f)
358 358 util.audit_path(f)
359 359 try:
360 360 util.unlink(repo.wjoin(f))
361 361 except OSError, inst:
362 362 if inst.errno != errno.ENOENT:
363 363 repo.ui.warn(_("update failed to remove %s: %s!\n") %
364 364 (f, inst.strerror))
365 365 removed += 1
366 366 elif m == "m": # merge
367 367 f2, fd, flag, move = a[2:]
368 368 r = filemerge(repo, f, f2, wctx, mctx)
369 369 if r > 0:
370 370 unresolved += 1
371 371 else:
372 372 if r is None:
373 373 updated += 1
374 374 else:
375 375 merged += 1
376 376 if f != fd:
377 377 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
378 378 repo.wwrite(fd, repo.wread(f))
379 379 if move:
380 380 repo.ui.debug(_("removing %s\n") % f)
381 381 os.unlink(repo.wjoin(f))
382 382 util.set_exec(repo.wjoin(fd), flag)
383 383 elif m == "g": # get
384 384 flag = a[2]
385 385 repo.ui.note(_("getting %s\n") % f)
386 386 t = mctx.filectx(f).data()
387 387 repo.wwrite(f, t)
388 388 util.set_exec(repo.wjoin(f), flag)
389 389 updated += 1
390 390 elif m == "d": # directory rename
391 391 f2, fd, flag = a[2:]
392 392 if f:
393 393 repo.ui.note(_("moving %s to %s\n") % (f, fd))
394 394 t = wctx.filectx(f).data()
395 395 repo.wwrite(fd, t)
396 396 util.set_exec(repo.wjoin(fd), flag)
397 397 util.unlink(repo.wjoin(f))
398 398 if f2:
399 399 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
400 400 t = mctx.filectx(f2).data()
401 401 repo.wwrite(fd, t)
402 402 util.set_exec(repo.wjoin(fd), flag)
403 403 updated += 1
404 404 elif m == "e": # exec
405 405 flag = a[2]
406 406 util.set_exec(repo.wjoin(f), flag)
407 407
408 408 return updated, merged, removed, unresolved
409 409
410 410 def recordupdates(repo, action, branchmerge):
411 411 "record merge actions to the dirstate"
412 412
413 413 for a in action:
414 414 f, m = a[:2]
415 415 if m == "r": # remove
416 416 if branchmerge:
417 417 repo.dirstate.update([f], 'r')
418 418 else:
419 419 repo.dirstate.forget([f])
420 420 elif m == "f": # forget
421 421 repo.dirstate.forget([f])
422 422 elif m == "g": # get
423 423 if branchmerge:
424 424 repo.dirstate.update([f], 'n', st_mtime=-1)
425 425 else:
426 426 repo.dirstate.update([f], 'n')
427 427 elif m == "m": # merge
428 428 f2, fd, flag, move = a[2:]
429 429 if branchmerge:
430 430 # We've done a branch merge, mark this file as merged
431 431 # so that we properly record the merger later
432 432 repo.dirstate.update([fd], 'm')
433 433 if f != f2: # copy/rename
434 434 if move:
435 435 repo.dirstate.update([f], 'r')
436 436 if f != fd:
437 437 repo.dirstate.copy(f, fd)
438 438 else:
439 439 repo.dirstate.copy(f2, fd)
440 440 else:
441 441 # We've update-merged a locally modified file, so
442 442 # we set the dirstate to emulate a normal checkout
443 443 # of that file some time in the past. Thus our
444 444 # merge will appear as a normal local file
445 445 # modification.
446 446 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
447 447 if move:
448 448 repo.dirstate.forget([f])
449 449 elif m == "d": # directory rename
450 450 f2, fd, flag = a[2:]
451 451 if branchmerge:
452 452 repo.dirstate.update([fd], 'a')
453 453 if f:
454 454 repo.dirstate.update([f], 'r')
455 455 repo.dirstate.copy(f, fd)
456 456 if f2:
457 457 repo.dirstate.copy(f2, fd)
458 458 else:
459 459 repo.dirstate.update([fd], 'n')
460 460 if f:
461 461 repo.dirstate.forget([f])
462 462
463 463 def update(repo, node, branchmerge, force, partial, wlock):
464 464 """
465 465 Perform a merge between the working directory and the given node
466 466
467 467 branchmerge = whether to merge between branches
468 468 force = whether to force branch merging or file overwriting
469 469 partial = a function to filter file lists (dirstate not updated)
470 470 wlock = working dir lock, if already held
471 471 """
472 472
473 473 if not wlock:
474 474 wlock = repo.wlock()
475 475
476 476 overwrite = force and not branchmerge
477 477 forcemerge = force and branchmerge
478 478 wc = repo.workingctx()
479 479 pl = wc.parents()
480 480 p1, p2 = pl[0], repo.changectx(node)
481 481 pa = p1.ancestor(p2)
482 482 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
483 483
484 484 ### check phase
485 485 if not overwrite and len(pl) > 1:
486 486 raise util.Abort(_("outstanding uncommitted merges"))
487 487 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
488 488 if branchmerge:
489 489 raise util.Abort(_("there is nothing to merge, just use "
490 490 "'hg update' or look at 'hg heads'"))
491 491 elif not (overwrite or branchmerge):
492 492 raise util.Abort(_("update spans branches, use 'hg merge' "
493 493 "or 'hg update -C' to lose changes"))
494 494 if branchmerge and not forcemerge:
495 495 if wc.files():
496 496 raise util.Abort(_("outstanding uncommitted changes"))
497 497
498 498 ### calculate phase
499 499 action = []
500 500 if not force:
501 501 checkunknown(wc, p2)
502 502 if not util.checkfolding(repo.path):
503 503 checkcollision(p2)
504 504 if not branchmerge:
505 505 action += forgetremoved(wc, p2)
506 506 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
507 507
508 508 ### apply phase
509 509 if not branchmerge: # just jump to the new rev
510 510 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
511 511 if not partial:
512 512 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
513 513
514 514 stats = applyupdates(repo, action, wc, p2)
515 515
516 516 if not partial:
517 517 recordupdates(repo, action, branchmerge)
518 518 repo.dirstate.setparents(fp1, fp2)
519 519 if not branchmerge:
520 520 repo.dirstate.setbranch(p2.branch())
521 521 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
522 522
523 523 return stats
524 524
General Comments 0
You need to be logged in to leave comments. Login now