##// END OF EJS Templates
correctly update dirstate after update+mode change (issue1456)
Benoit Boissinot -
r7569:89207edf default
parent child Browse files
Show More
@@ -0,0 +1,17 b''
1 #!/bin/sh
2
3 rm -rf a
4 hg init a
5 cd a
6
7 echo foo > foo
8 hg ci -qAm0
9 chmod +x foo
10 hg ci -m1
11 hg co -q 0
12 echo dirty > foo
13 sleep 1
14 hg up -q
15 cat foo
16 hg st -A
17
@@ -0,0 +1,2 b''
1 dirty
2 M foo
@@ -1,500 +1,502 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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import nullid, nullrev, hex, bin
9 9 from i18n import _
10 10 import errno, util, os, filemerge, copies, shutil
11 11
12 12 class mergestate(object):
13 13 '''track 3-way merge state of individual files'''
14 14 def __init__(self, repo):
15 15 self._repo = repo
16 16 self._read()
17 17 def reset(self, node):
18 18 self._state = {}
19 19 self._local = node
20 20 shutil.rmtree(self._repo.join("merge"), True)
21 21 def _read(self):
22 22 self._state = {}
23 23 try:
24 24 localnode = None
25 25 f = self._repo.opener("merge/state")
26 26 for i, l in enumerate(f):
27 27 if i == 0:
28 28 localnode = l[:-1]
29 29 else:
30 30 bits = l[:-1].split("\0")
31 31 self._state[bits[0]] = bits[1:]
32 32 self._local = bin(localnode)
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.items():
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 repo.ui.note(_("resolving manifests\n"))
127 127 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
128 128 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
129 129
130 130 m1 = p1.manifest()
131 131 m2 = p2.manifest()
132 132 ma = pa.manifest()
133 133 backwards = (pa == p2)
134 134 action = []
135 135 copy, copied, diverge = {}, {}, {}
136 136
137 137 def fmerge(f, f2=None, fa=None):
138 138 """merge flags"""
139 139 if not f2:
140 140 f2 = f
141 141 fa = f
142 142 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
143 143 if m == n: # flags agree
144 144 return m # unchanged
145 145 if m and n: # flags are set but don't agree
146 146 if not a: # both differ from parent
147 147 r = repo.ui.prompt(
148 148 _(" conflicting flags for %s\n"
149 149 "(n)one, e(x)ec or sym(l)ink?") % f, "[nxl]", "n")
150 150 return r != "n" and r or ''
151 151 if m == a:
152 152 return n # changed from m to n
153 153 return m # changed from n to m
154 154 if m and m != a: # changed from a to m
155 155 return m
156 156 if n and n != a: # changed from a to n
157 157 return n
158 158 return '' # flag was cleared
159 159
160 160 def act(msg, m, f, *args):
161 161 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
162 162 action.append((f, m) + args)
163 163
164 164 if pa and not (backwards or overwrite):
165 165 if repo.ui.configbool("merge", "followcopies", True):
166 166 dirs = repo.ui.configbool("merge", "followdirs", True)
167 167 copy, diverge = copies.copies(repo, p1, p2, pa, dirs)
168 168 copied = dict.fromkeys(copy.values())
169 169 for of, fl in diverge.items():
170 170 act("divergent renames", "dr", of, fl)
171 171
172 172 # Compare manifests
173 173 for f, n in m1.iteritems():
174 174 if partial and not partial(f):
175 175 continue
176 176 if f in m2:
177 177 if overwrite or backwards:
178 178 rflags = m2.flags(f)
179 179 else:
180 180 rflags = fmerge(f)
181 181 # are files different?
182 182 if n != m2[f]:
183 183 a = ma.get(f, nullid)
184 184 # are we clobbering?
185 185 if overwrite:
186 186 act("clobbering", "g", f, rflags)
187 187 # or are we going back in time and clean?
188 188 elif backwards:
189 189 if not n[20:] or not p2[f].cmp(p1[f].data()):
190 190 act("reverting", "g", f, rflags)
191 191 # are both different from the ancestor?
192 192 elif n != a and m2[f] != a:
193 193 act("versions differ", "m", f, f, f, rflags, False)
194 194 # is remote's version newer?
195 195 elif m2[f] != a:
196 196 act("remote is newer", "g", f, rflags)
197 197 # local is newer, not overwrite, check mode bits
198 198 elif m1.flags(f) != rflags:
199 199 act("update permissions", "e", f, rflags)
200 200 # contents same, check mode bits
201 201 elif m1.flags(f) != rflags:
202 202 act("update permissions", "e", f, rflags)
203 203 elif f in copied:
204 204 continue
205 205 elif f in copy:
206 206 f2 = copy[f]
207 207 if f2 not in m2: # directory rename
208 208 act("remote renamed directory to " + f2, "d",
209 209 f, None, f2, m1.flags(f))
210 210 elif f2 in m1: # case 2 A,B/B/B
211 211 act("local copied to " + f2, "m",
212 212 f, f2, f, fmerge(f, f2, f2), False)
213 213 else: # case 4,21 A/B/B
214 214 act("local moved to " + f2, "m",
215 215 f, f2, f, fmerge(f, f2, f2), False)
216 216 elif f in ma:
217 217 if n != ma[f] and not overwrite:
218 218 if repo.ui.prompt(
219 219 _(" local changed %s which remote deleted\n"
220 220 "use (c)hanged version or (d)elete?") % f,
221 221 _("[cd]"), _("c")) == _("d"):
222 222 act("prompt delete", "r", f)
223 223 else:
224 224 act("other deleted", "r", f)
225 225 else:
226 226 # file is created on branch or in working directory
227 227 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
228 228 act("remote deleted", "r", f)
229 229
230 230 for f, n in m2.iteritems():
231 231 if partial and not partial(f):
232 232 continue
233 233 if f in m1:
234 234 continue
235 235 if f in copied:
236 236 continue
237 237 if f in copy:
238 238 f2 = copy[f]
239 239 if f2 not in m1: # directory rename
240 240 act("local renamed directory to " + f2, "d",
241 241 None, f, f2, m2.flags(f))
242 242 elif f2 in m2: # rename case 1, A/A,B/A
243 243 act("remote copied to " + f, "m",
244 244 f2, f, f, fmerge(f2, f, f2), False)
245 245 else: # case 3,20 A/B/A
246 246 act("remote moved to " + f, "m",
247 247 f2, f, f, fmerge(f2, f, f2), True)
248 248 elif f in ma:
249 249 if overwrite or backwards:
250 250 act("recreating", "g", f, m2.flags(f))
251 251 elif n != ma[f]:
252 252 if repo.ui.prompt(
253 253 _("remote changed %s which local deleted\n"
254 254 "use (c)hanged version or leave (d)eleted?") % f,
255 255 _("[cd]"), _("c")) == _("c"):
256 256 act("prompt recreating", "g", f, m2.flags(f))
257 257 else:
258 258 act("remote created", "g", f, m2.flags(f))
259 259
260 260 return action
261 261
262 262 def actioncmp(a1, a2):
263 263 m1 = a1[1]
264 264 m2 = a2[1]
265 265 if m1 == m2:
266 266 return cmp(a1, a2)
267 267 if m1 == 'r':
268 268 return -1
269 269 if m2 == 'r':
270 270 return 1
271 271 return cmp(a1, a2)
272 272
273 273 def applyupdates(repo, action, wctx, mctx):
274 274 "apply the merge action list to the working directory"
275 275
276 276 updated, merged, removed, unresolved = 0, 0, 0, 0
277 277 ms = mergestate(repo)
278 278 ms.reset(wctx.parents()[0].node())
279 279 moves = []
280 280 action.sort(actioncmp)
281 281
282 282 # prescan for merges
283 283 for a in action:
284 284 f, m = a[:2]
285 285 if m == 'm': # merge
286 286 f2, fd, flags, move = a[2:]
287 287 repo.ui.debug(_("preserving %s for resolve of %s\n") % (f, fd))
288 288 fcl = wctx[f]
289 289 fco = mctx[f2]
290 290 fca = fcl.ancestor(fco) or repo.filectx(f, fileid=nullrev)
291 291 ms.add(fcl, fco, fca, fd, flags)
292 292 if f != fd and move:
293 293 moves.append(f)
294 294
295 295 # remove renamed files after safely stored
296 296 for f in moves:
297 297 if util.lexists(repo.wjoin(f)):
298 298 repo.ui.debug(_("removing %s\n") % f)
299 299 os.unlink(repo.wjoin(f))
300 300
301 301 audit_path = util.path_auditor(repo.root)
302 302
303 303 for a in action:
304 304 f, m = a[:2]
305 305 if f and f[0] == "/":
306 306 continue
307 307 if m == "r": # remove
308 308 repo.ui.note(_("removing %s\n") % f)
309 309 audit_path(f)
310 310 try:
311 311 util.unlink(repo.wjoin(f))
312 312 except OSError, inst:
313 313 if inst.errno != errno.ENOENT:
314 314 repo.ui.warn(_("update failed to remove %s: %s!\n") %
315 315 (f, inst.strerror))
316 316 removed += 1
317 317 elif m == "m": # merge
318 318 f2, fd, flags, move = a[2:]
319 319 r = ms.resolve(fd, wctx, mctx)
320 320 if r > 0:
321 321 unresolved += 1
322 322 else:
323 323 if r is None:
324 324 updated += 1
325 325 else:
326 326 merged += 1
327 327 util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
328 328 if f != fd and move and util.lexists(repo.wjoin(f)):
329 329 repo.ui.debug(_("removing %s\n") % f)
330 330 os.unlink(repo.wjoin(f))
331 331 elif m == "g": # get
332 332 flags = a[2]
333 333 repo.ui.note(_("getting %s\n") % f)
334 334 t = mctx.filectx(f).data()
335 335 repo.wwrite(f, t, flags)
336 336 updated += 1
337 337 elif m == "d": # directory rename
338 338 f2, fd, flags = a[2:]
339 339 if f:
340 340 repo.ui.note(_("moving %s to %s\n") % (f, fd))
341 341 t = wctx.filectx(f).data()
342 342 repo.wwrite(fd, t, flags)
343 343 util.unlink(repo.wjoin(f))
344 344 if f2:
345 345 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
346 346 t = mctx.filectx(f2).data()
347 347 repo.wwrite(fd, t, flags)
348 348 updated += 1
349 349 elif m == "dr": # divergent renames
350 350 fl = a[2]
351 351 repo.ui.warn(_("warning: detected divergent renames of %s to:\n") % f)
352 352 for nf in fl:
353 353 repo.ui.warn(" %s\n" % nf)
354 354 elif m == "e": # exec
355 355 flags = a[2]
356 356 util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags)
357 357
358 358 return updated, merged, removed, unresolved
359 359
360 360 def recordupdates(repo, action, branchmerge):
361 361 "record merge actions to the dirstate"
362 362
363 363 for a in action:
364 364 f, m = a[:2]
365 365 if m == "r": # remove
366 366 if branchmerge:
367 367 repo.dirstate.remove(f)
368 368 else:
369 369 repo.dirstate.forget(f)
370 370 elif m == "f": # forget
371 371 repo.dirstate.forget(f)
372 elif m in "ge": # get or exec change
372 elif m == "e": # exec change
373 repo.dirstate.normaldirty(f)
374 elif m == "g": # get
373 375 if branchmerge:
374 376 repo.dirstate.normaldirty(f)
375 377 else:
376 378 repo.dirstate.normal(f)
377 379 elif m == "m": # merge
378 380 f2, fd, flag, move = a[2:]
379 381 if branchmerge:
380 382 # We've done a branch merge, mark this file as merged
381 383 # so that we properly record the merger later
382 384 repo.dirstate.merge(fd)
383 385 if f != f2: # copy/rename
384 386 if move:
385 387 repo.dirstate.remove(f)
386 388 if f != fd:
387 389 repo.dirstate.copy(f, fd)
388 390 else:
389 391 repo.dirstate.copy(f2, fd)
390 392 else:
391 393 # We've update-merged a locally modified file, so
392 394 # we set the dirstate to emulate a normal checkout
393 395 # of that file some time in the past. Thus our
394 396 # merge will appear as a normal local file
395 397 # modification.
396 398 repo.dirstate.normallookup(fd)
397 399 if move:
398 400 repo.dirstate.forget(f)
399 401 elif m == "d": # directory rename
400 402 f2, fd, flag = a[2:]
401 403 if not f2 and f not in repo.dirstate:
402 404 # untracked file moved
403 405 continue
404 406 if branchmerge:
405 407 repo.dirstate.add(fd)
406 408 if f:
407 409 repo.dirstate.remove(f)
408 410 repo.dirstate.copy(f, fd)
409 411 if f2:
410 412 repo.dirstate.copy(f2, fd)
411 413 else:
412 414 repo.dirstate.normal(fd)
413 415 if f:
414 416 repo.dirstate.forget(f)
415 417
416 418 def update(repo, node, branchmerge, force, partial):
417 419 """
418 420 Perform a merge between the working directory and the given node
419 421
420 422 branchmerge = whether to merge between branches
421 423 force = whether to force branch merging or file overwriting
422 424 partial = a function to filter file lists (dirstate not updated)
423 425 """
424 426
425 427 wlock = repo.wlock()
426 428 try:
427 429 wc = repo[None]
428 430 if node is None:
429 431 # tip of current branch
430 432 try:
431 433 node = repo.branchtags()[wc.branch()]
432 434 except KeyError:
433 435 if wc.branch() == "default": # no default branch!
434 436 node = repo.lookup("tip") # update to tip
435 437 else:
436 438 raise util.Abort(_("branch %s not found") % wc.branch())
437 439 overwrite = force and not branchmerge
438 440 pl = wc.parents()
439 441 p1, p2 = pl[0], repo[node]
440 442 pa = p1.ancestor(p2)
441 443 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
442 444 fastforward = False
443 445
444 446 ### check phase
445 447 if not overwrite and len(pl) > 1:
446 448 raise util.Abort(_("outstanding uncommitted merges"))
447 449 if branchmerge:
448 450 if pa == p2:
449 451 raise util.Abort(_("can't merge with ancestor"))
450 452 elif pa == p1:
451 453 if p1.branch() != p2.branch():
452 454 fastforward = True
453 455 else:
454 456 raise util.Abort(_("nothing to merge (use 'hg update'"
455 457 " or check 'hg heads')"))
456 458 if not force and (wc.files() or wc.deleted()):
457 459 raise util.Abort(_("outstanding uncommitted changes"))
458 460 elif not overwrite:
459 461 if pa == p1 or pa == p2: # linear
460 462 pass # all good
461 463 elif p1.branch() == p2.branch():
462 464 if wc.files() or wc.deleted():
463 465 raise util.Abort(_("crosses branches (use 'hg merge' or "
464 466 "'hg update -C' to discard changes)"))
465 467 raise util.Abort(_("crosses branches (use 'hg merge' "
466 468 "or 'hg update -C')"))
467 469 elif wc.files() or wc.deleted():
468 470 raise util.Abort(_("crosses named branches (use "
469 471 "'hg update -C' to discard changes)"))
470 472 else:
471 473 # Allow jumping branches if there are no changes
472 474 overwrite = True
473 475
474 476 ### calculate phase
475 477 action = []
476 478 if not force:
477 479 _checkunknown(wc, p2)
478 480 if not util.checkcase(repo.path):
479 481 _checkcollision(p2)
480 482 action += _forgetremoved(wc, p2, branchmerge)
481 483 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
482 484
483 485 ### apply phase
484 486 if not branchmerge: # just jump to the new rev
485 487 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
486 488 if not partial:
487 489 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
488 490
489 491 stats = applyupdates(repo, action, wc, p2)
490 492
491 493 if not partial:
492 494 recordupdates(repo, action, branchmerge)
493 495 repo.dirstate.setparents(fp1, fp2)
494 496 if not branchmerge and not fastforward:
495 497 repo.dirstate.setbranch(p2.branch())
496 498 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
497 499
498 500 return stats
499 501 finally:
500 502 del wlock
General Comments 0
You need to be logged in to leave comments. Login now