##// END OF EJS Templates
merge: fix adding untracked files on directory rename (issue612)...
Matt Mackall -
r4819:97971245 default
parent child Browse files
Show More
@@ -0,0 +1,24
1 #!/bin/sh
2
3 mkdir t
4 cd t
5
6 hg init
7 mkdir src
8 echo a > src/a.c
9 hg ci -Ama -d "10000000 0"
10
11 hg mv src source
12 hg ci -Ammove -d "1000000 0"
13
14 hg co -C 0
15 echo new > src/a.c
16 echo compiled > src/a.o
17 hg ci -mupdate -d "1000000 0"
18
19 hg st
20
21 hg merge
22
23 hg st
24
@@ -0,0 +1,11
1 adding src/a.c
2 copying src/a.c to source/a.c
3 removing src/a.c
4 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
5 ? src/a.o
6 merging src/a.c and source/a.c
7 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
8 (branch merge, don't forget to commit)
9 M source/a.c
10 R src/a.c
11 ? source/a.o
@@ -1,564 +1,567
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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import _
10 10 import errno, util, os, tempfile, context
11 11
12 12 def filemerge(repo, fw, fo, wctx, mctx):
13 13 """perform a 3-way merge in the working directory
14 14
15 15 fw = filename in the working directory
16 16 fo = filename in other parent
17 17 wctx, mctx = working and merge changecontexts
18 18 """
19 19
20 20 def temp(prefix, ctx):
21 21 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
22 22 (fd, name) = tempfile.mkstemp(prefix=pre)
23 23 data = repo.wwritedata(ctx.path(), ctx.data())
24 24 f = os.fdopen(fd, "wb")
25 25 f.write(data)
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 nonoverlap(d1, d2, d3):
106 106 "Return list of elements in d1 not in d2 or d3"
107 107 l = [d for d in d1 if d not in d3 and d not in d2]
108 108 l.sort()
109 109 return l
110 110
111 111 def dirname(f):
112 112 s = f.rfind("/")
113 113 if s == -1:
114 114 return ""
115 115 return f[:s]
116 116
117 117 def dirs(files):
118 118 d = {}
119 119 for f in files:
120 120 f = dirname(f)
121 121 while f not in d:
122 122 d[f] = True
123 123 f = dirname(f)
124 124 return d
125 125
126 126 wctx = repo.workingctx()
127 127
128 128 def makectx(f, n):
129 129 if len(n) == 20:
130 130 return repo.filectx(f, fileid=n)
131 131 return wctx.filectx(f)
132 132 ctx = util.cachefunc(makectx)
133 133
134 134 def findold(fctx):
135 135 "find files that path was copied from, back to linkrev limit"
136 136 old = {}
137 137 seen = {}
138 138 orig = fctx.path()
139 139 visit = [fctx]
140 140 while visit:
141 141 fc = visit.pop()
142 142 s = str(fc)
143 143 if s in seen:
144 144 continue
145 145 seen[s] = 1
146 146 if fc.path() != orig and fc.path() not in old:
147 147 old[fc.path()] = 1
148 148 if fc.rev() < limit:
149 149 continue
150 150 visit += fc.parents()
151 151
152 152 old = old.keys()
153 153 old.sort()
154 154 return old
155 155
156 156 copy = {}
157 157 fullcopy = {}
158 158 diverge = {}
159 159
160 160 def checkcopies(c, man):
161 161 '''check possible copies for filectx c'''
162 162 for of in findold(c):
163 163 fullcopy[c.path()] = of # remember for dir rename detection
164 164 if of not in man: # original file not in other manifest?
165 165 if of in ma:
166 166 diverge.setdefault(of, []).append(c.path())
167 167 continue
168 168 c2 = ctx(of, man[of])
169 169 ca = c.ancestor(c2)
170 170 if not ca: # unrelated?
171 171 continue
172 172 # named changed on only one side?
173 173 if ca.path() == c.path() or ca.path() == c2.path():
174 174 if c == ca or c2 == ca: # no merge needed, ignore copy
175 175 continue
176 176 copy[c.path()] = of
177 177
178 178 if not repo.ui.configbool("merge", "followcopies", True):
179 179 return {}, {}
180 180
181 181 # avoid silly behavior for update from empty dir
182 182 if not m1 or not m2 or not ma:
183 183 return {}, {}
184 184
185 185 u1 = nonoverlap(m1, m2, ma)
186 186 u2 = nonoverlap(m2, m1, ma)
187 187
188 188 for f in u1:
189 189 checkcopies(ctx(f, m1[f]), m2)
190 190
191 191 for f in u2:
192 192 checkcopies(ctx(f, m2[f]), m1)
193 193
194 194 d2 = {}
195 195 for of, fl in diverge.items():
196 196 for f in fl:
197 197 fo = list(fl)
198 198 fo.remove(f)
199 199 d2[f] = (of, fo)
200 200 #diverge = d2
201 201
202 202 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
203 203 return copy, diverge
204 204
205 205 # generate a directory move map
206 206 d1, d2 = dirs(m1), dirs(m2)
207 207 invalid = {}
208 208 dirmove = {}
209 209
210 210 # examine each file copy for a potential directory move, which is
211 211 # when all the files in a directory are moved to a new directory
212 212 for dst, src in fullcopy.items():
213 213 dsrc, ddst = dirname(src), dirname(dst)
214 214 if dsrc in invalid:
215 215 # already seen to be uninteresting
216 216 continue
217 217 elif dsrc in d1 and ddst in d1:
218 218 # directory wasn't entirely moved locally
219 219 invalid[dsrc] = True
220 220 elif dsrc in d2 and ddst in d2:
221 221 # directory wasn't entirely moved remotely
222 222 invalid[dsrc] = True
223 223 elif dsrc in dirmove and dirmove[dsrc] != ddst:
224 224 # files from the same directory moved to two different places
225 225 invalid[dsrc] = True
226 226 else:
227 227 # looks good so far
228 228 dirmove[dsrc + "/"] = ddst + "/"
229 229
230 230 for i in invalid:
231 231 if i in dirmove:
232 232 del dirmove[i]
233 233
234 234 del d1, d2, invalid
235 235
236 236 if not dirmove:
237 237 return copy, diverge
238 238
239 239 # check unaccounted nonoverlapping files against directory moves
240 240 for f in u1 + u2:
241 241 if f not in fullcopy:
242 242 for d in dirmove:
243 243 if f.startswith(d):
244 244 # new file added in a directory that was moved, move it
245 245 copy[f] = dirmove[d] + f[len(d):]
246 246 break
247 247
248 248 return copy, diverge
249 249
250 250 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
251 251 """
252 252 Merge p1 and p2 with ancestor ma and generate merge action list
253 253
254 254 overwrite = whether we clobber working files
255 255 partial = function to filter file lists
256 256 """
257 257
258 258 repo.ui.note(_("resolving manifests\n"))
259 259 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
260 260 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
261 261
262 262 m1 = p1.manifest()
263 263 m2 = p2.manifest()
264 264 ma = pa.manifest()
265 265 backwards = (pa == p2)
266 266 action = []
267 267 copy = {}
268 268 diverge = {}
269 269
270 270 def fmerge(f, f2=None, fa=None):
271 271 """merge flags"""
272 272 if not f2:
273 273 f2 = f
274 274 fa = f
275 275 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
276 276 if ((a^b) | (a^c)) ^ a:
277 277 return 'x'
278 278 a, b, c = ma.linkf(fa), m1.linkf(f), m2.linkf(f2)
279 279 if ((a^b) | (a^c)) ^ a:
280 280 return 'l'
281 281 return ''
282 282
283 283 def act(msg, m, f, *args):
284 284 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
285 285 action.append((f, m) + args)
286 286
287 287 if not (backwards or overwrite):
288 288 copy, diverge = findcopies(repo, m1, m2, ma, pa.rev())
289 289
290 290 for of, fl in diverge.items():
291 291 act("divergent renames", "dr", of, fl)
292 292
293 293 copied = dict.fromkeys(copy.values())
294 294
295 295 # Compare manifests
296 296 for f, n in m1.iteritems():
297 297 if partial and not partial(f):
298 298 continue
299 299 if f in m2:
300 300 # are files different?
301 301 if n != m2[f]:
302 302 a = ma.get(f, nullid)
303 303 # are both different from the ancestor?
304 304 if not overwrite and n != a and m2[f] != a:
305 305 act("versions differ", "m", f, f, f, fmerge(f), False)
306 306 # are we clobbering?
307 307 # is remote's version newer?
308 308 # or are we going back in time and clean?
309 309 elif overwrite or m2[f] != a or (backwards and not n[20:]):
310 310 act("remote is newer", "g", f, m2.flags(f))
311 311 # local is newer, not overwrite, check mode bits
312 312 elif fmerge(f) != m1.flags(f):
313 313 act("update permissions", "e", f, m2.flags(f))
314 314 # contents same, check mode bits
315 315 elif m1.flags(f) != m2.flags(f):
316 316 if overwrite or fmerge(f) != m1.flags(f):
317 317 act("update permissions", "e", f, m2.flags(f))
318 318 elif f in copied:
319 319 continue
320 320 elif f in copy:
321 321 f2 = copy[f]
322 322 if f2 not in m2: # directory rename
323 323 act("remote renamed directory to " + f2, "d",
324 324 f, None, f2, m1.flags(f))
325 325 elif f2 in m1: # case 2 A,B/B/B
326 326 act("local copied to " + f2, "m",
327 327 f, f2, f, fmerge(f, f2, f2), False)
328 328 else: # case 4,21 A/B/B
329 329 act("local moved to " + f2, "m",
330 330 f, f2, f, fmerge(f, f2, f2), False)
331 331 elif f in ma:
332 332 if n != ma[f] and not overwrite:
333 333 if repo.ui.prompt(
334 334 (_(" local changed %s which remote deleted\n") % f) +
335 335 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
336 336 act("prompt delete", "r", f)
337 337 else:
338 338 act("other deleted", "r", f)
339 339 else:
340 340 # file is created on branch or in working directory
341 341 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
342 342 act("remote deleted", "r", f)
343 343
344 344 for f, n in m2.iteritems():
345 345 if partial and not partial(f):
346 346 continue
347 347 if f in m1:
348 348 continue
349 349 if f in copied:
350 350 continue
351 351 if f in copy:
352 352 f2 = copy[f]
353 353 if f2 not in m1: # directory rename
354 354 act("local renamed directory to " + f2, "d",
355 355 None, f, f2, m2.flags(f))
356 356 elif f2 in m2: # rename case 1, A/A,B/A
357 357 act("remote copied to " + f, "m",
358 358 f2, f, f, fmerge(f2, f, f2), False)
359 359 else: # case 3,20 A/B/A
360 360 act("remote moved to " + f, "m",
361 361 f2, f, f, fmerge(f2, f, f2), True)
362 362 elif f in ma:
363 363 if overwrite or backwards:
364 364 act("recreating", "g", f, m2.flags(f))
365 365 elif n != ma[f]:
366 366 if repo.ui.prompt(
367 367 (_("remote changed %s which local deleted\n") % f) +
368 368 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
369 369 act("prompt recreating", "g", f, m2.flags(f))
370 370 else:
371 371 act("remote created", "g", f, m2.flags(f))
372 372
373 373 return action
374 374
375 375 def applyupdates(repo, action, wctx, mctx):
376 376 "apply the merge action list to the working directory"
377 377
378 378 updated, merged, removed, unresolved = 0, 0, 0, 0
379 379 action.sort()
380 380 for a in action:
381 381 f, m = a[:2]
382 382 if f and f[0] == "/":
383 383 continue
384 384 if m == "r": # remove
385 385 repo.ui.note(_("removing %s\n") % f)
386 386 util.audit_path(f)
387 387 try:
388 388 util.unlink(repo.wjoin(f))
389 389 except OSError, inst:
390 390 if inst.errno != errno.ENOENT:
391 391 repo.ui.warn(_("update failed to remove %s: %s!\n") %
392 392 (f, inst.strerror))
393 393 removed += 1
394 394 elif m == "m": # merge
395 395 f2, fd, flags, move = a[2:]
396 396 r = filemerge(repo, f, f2, wctx, mctx)
397 397 if r > 0:
398 398 unresolved += 1
399 399 else:
400 400 if r is None:
401 401 updated += 1
402 402 else:
403 403 merged += 1
404 404 if f != fd:
405 405 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
406 406 repo.wwrite(fd, repo.wread(f), flags)
407 407 if move:
408 408 repo.ui.debug(_("removing %s\n") % f)
409 409 os.unlink(repo.wjoin(f))
410 410 util.set_exec(repo.wjoin(fd), "x" in flags)
411 411 elif m == "g": # get
412 412 flags = a[2]
413 413 repo.ui.note(_("getting %s\n") % f)
414 414 t = mctx.filectx(f).data()
415 415 repo.wwrite(f, t, flags)
416 416 updated += 1
417 417 elif m == "d": # directory rename
418 418 f2, fd, flags = a[2:]
419 419 if f:
420 420 repo.ui.note(_("moving %s to %s\n") % (f, fd))
421 421 t = wctx.filectx(f).data()
422 422 repo.wwrite(fd, t, flags)
423 423 util.unlink(repo.wjoin(f))
424 424 if f2:
425 425 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
426 426 t = mctx.filectx(f2).data()
427 427 repo.wwrite(fd, t, flags)
428 428 updated += 1
429 429 elif m == "dr": # divergent renames
430 430 fl = a[2]
431 431 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
432 432 for nf in fl:
433 433 repo.ui.warn(" %s\n" % nf)
434 434 elif m == "e": # exec
435 435 flags = a[2]
436 436 util.set_exec(repo.wjoin(f), flags)
437 437
438 438 return updated, merged, removed, unresolved
439 439
440 440 def recordupdates(repo, action, branchmerge):
441 441 "record merge actions to the dirstate"
442 442
443 443 for a in action:
444 444 f, m = a[:2]
445 445 if m == "r": # remove
446 446 if branchmerge:
447 447 repo.dirstate.update([f], 'r')
448 448 else:
449 449 repo.dirstate.forget([f])
450 450 elif m == "f": # forget
451 451 repo.dirstate.forget([f])
452 452 elif m == "g": # get
453 453 if branchmerge:
454 454 repo.dirstate.update([f], 'n', st_mtime=-1)
455 455 else:
456 456 repo.dirstate.update([f], 'n')
457 457 elif m == "m": # merge
458 458 f2, fd, flag, move = a[2:]
459 459 if branchmerge:
460 460 # We've done a branch merge, mark this file as merged
461 461 # so that we properly record the merger later
462 462 repo.dirstate.update([fd], 'm')
463 463 if f != f2: # copy/rename
464 464 if move:
465 465 repo.dirstate.update([f], 'r')
466 466 if f != fd:
467 467 repo.dirstate.copy(f, fd)
468 468 else:
469 469 repo.dirstate.copy(f2, fd)
470 470 else:
471 471 # We've update-merged a locally modified file, so
472 472 # we set the dirstate to emulate a normal checkout
473 473 # of that file some time in the past. Thus our
474 474 # merge will appear as a normal local file
475 475 # modification.
476 476 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
477 477 if move:
478 478 repo.dirstate.forget([f])
479 479 elif m == "d": # directory rename
480 480 f2, fd, flag = a[2:]
481 if not f2 and f not in repo.dirstate:
482 # untracked file moved
483 continue
481 484 if branchmerge:
482 485 repo.dirstate.update([fd], 'a')
483 486 if f:
484 487 repo.dirstate.update([f], 'r')
485 488 repo.dirstate.copy(f, fd)
486 489 if f2:
487 490 repo.dirstate.copy(f2, fd)
488 491 else:
489 492 repo.dirstate.update([fd], 'n')
490 493 if f:
491 494 repo.dirstate.forget([f])
492 495
493 496 def update(repo, node, branchmerge, force, partial, wlock):
494 497 """
495 498 Perform a merge between the working directory and the given node
496 499
497 500 branchmerge = whether to merge between branches
498 501 force = whether to force branch merging or file overwriting
499 502 partial = a function to filter file lists (dirstate not updated)
500 503 wlock = working dir lock, if already held
501 504 """
502 505
503 506 if not wlock:
504 507 wlock = repo.wlock()
505 508
506 509 wc = repo.workingctx()
507 510 if node is None:
508 511 # tip of current branch
509 512 try:
510 513 node = repo.branchtags()[wc.branch()]
511 514 except KeyError:
512 515 raise util.Abort(_("branch %s not found") % wc.branch())
513 516 overwrite = force and not branchmerge
514 517 forcemerge = force and branchmerge
515 518 pl = wc.parents()
516 519 p1, p2 = pl[0], repo.changectx(node)
517 520 pa = p1.ancestor(p2)
518 521 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
519 522 fastforward = False
520 523
521 524 ### check phase
522 525 if not overwrite and len(pl) > 1:
523 526 raise util.Abort(_("outstanding uncommitted merges"))
524 527 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
525 528 if branchmerge:
526 529 if p1.branch() != p2.branch() and pa != p2:
527 530 fastforward = True
528 531 else:
529 532 raise util.Abort(_("there is nothing to merge, just use "
530 533 "'hg update' or look at 'hg heads'"))
531 534 elif not (overwrite or branchmerge):
532 535 raise util.Abort(_("update spans branches, use 'hg merge' "
533 536 "or 'hg update -C' to lose changes"))
534 537 if branchmerge and not forcemerge:
535 538 if wc.files():
536 539 raise util.Abort(_("outstanding uncommitted changes"))
537 540
538 541 ### calculate phase
539 542 action = []
540 543 if not force:
541 544 checkunknown(wc, p2)
542 545 if not util.checkfolding(repo.path):
543 546 checkcollision(p2)
544 547 if not branchmerge:
545 548 action += forgetremoved(wc, p2)
546 549 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
547 550
548 551 ### apply phase
549 552 if not branchmerge: # just jump to the new rev
550 553 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
551 554 if not partial:
552 555 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
553 556
554 557 stats = applyupdates(repo, action, wc, p2)
555 558
556 559 if not partial:
557 560 recordupdates(repo, action, branchmerge)
558 561 repo.dirstate.setparents(fp1, fp2)
559 562 if not branchmerge and not fastforward:
560 563 repo.dirstate.setbranch(p2.branch())
561 564 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
562 565
563 566 return stats
564 567
General Comments 0
You need to be logged in to leave comments. Login now