##// END OF EJS Templates
applyupdates: audit unlinking of renamed files and directories
Adrian Buehlmann -
r14398:ae1f7a53 default
parent child Browse files
Show More
@@ -1,561 +1,564
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 of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import nullid, nullrev, hex, bin
9 9 from i18n import _
10 10 import scmutil, util, filemerge, copies, subrepo
11 11 import errno, os, shutil
12 12
13 13 class mergestate(object):
14 14 '''track 3-way merge state of individual files'''
15 15 def __init__(self, repo):
16 16 self._repo = repo
17 17 self._dirty = False
18 18 self._read()
19 19 def reset(self, node=None):
20 20 self._state = {}
21 21 if node:
22 22 self._local = node
23 23 shutil.rmtree(self._repo.join("merge"), True)
24 24 self._dirty = False
25 25 def _read(self):
26 26 self._state = {}
27 27 try:
28 28 f = self._repo.opener("merge/state")
29 29 for i, l in enumerate(f):
30 30 if i == 0:
31 31 self._local = bin(l[:-1])
32 32 else:
33 33 bits = l[:-1].split("\0")
34 34 self._state[bits[0]] = bits[1:]
35 35 f.close()
36 36 except IOError, err:
37 37 if err.errno != errno.ENOENT:
38 38 raise
39 39 self._dirty = False
40 40 def commit(self):
41 41 if self._dirty:
42 42 f = self._repo.opener("merge/state", "w")
43 43 f.write(hex(self._local) + "\n")
44 44 for d, v in self._state.iteritems():
45 45 f.write("\0".join([d] + v) + "\n")
46 46 f.close()
47 47 self._dirty = False
48 48 def add(self, fcl, fco, fca, fd, flags):
49 49 hash = util.sha1(fcl.path()).hexdigest()
50 50 self._repo.opener.write("merge/" + hash, fcl.data())
51 51 self._state[fd] = ['u', hash, fcl.path(), fca.path(),
52 52 hex(fca.filenode()), fco.path(), flags]
53 53 self._dirty = True
54 54 def __contains__(self, dfile):
55 55 return dfile in self._state
56 56 def __getitem__(self, dfile):
57 57 return self._state[dfile][0]
58 58 def __iter__(self):
59 59 l = self._state.keys()
60 60 l.sort()
61 61 for f in l:
62 62 yield f
63 63 def mark(self, dfile, state):
64 64 self._state[dfile][0] = state
65 65 self._dirty = True
66 66 def resolve(self, dfile, wctx, octx):
67 67 if self[dfile] == 'r':
68 68 return 0
69 69 state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
70 70 f = self._repo.opener("merge/" + hash)
71 71 self._repo.wwrite(dfile, f.read(), flags)
72 72 f.close()
73 73 fcd = wctx[dfile]
74 74 fco = octx[ofile]
75 75 fca = self._repo.filectx(afile, fileid=anode)
76 76 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
77 77 if r is None:
78 78 # no real conflict
79 79 del self._state[dfile]
80 80 elif not r:
81 81 self.mark(dfile, 'r')
82 82 return r
83 83
84 84 def _checkunknown(wctx, mctx):
85 85 "check for collisions between unknown files and files in mctx"
86 86 for f in wctx.unknown():
87 87 if f in mctx and mctx[f].cmp(wctx[f]):
88 88 raise util.Abort(_("untracked file in working directory differs"
89 89 " from file in requested revision: '%s'") % f)
90 90
91 91 def _checkcollision(mctx):
92 92 "check for case folding collisions in the destination context"
93 93 folded = {}
94 94 for fn in mctx:
95 95 fold = fn.lower()
96 96 if fold in folded:
97 97 raise util.Abort(_("case-folding collision between %s and %s")
98 98 % (fn, folded[fold]))
99 99 folded[fold] = fn
100 100
101 101 def _forgetremoved(wctx, mctx, branchmerge):
102 102 """
103 103 Forget removed files
104 104
105 105 If we're jumping between revisions (as opposed to merging), and if
106 106 neither the working directory nor the target rev has the file,
107 107 then we need to remove it from the dirstate, to prevent the
108 108 dirstate from listing the file when it is no longer in the
109 109 manifest.
110 110
111 111 If we're merging, and the other revision has removed a file
112 112 that is not present in the working directory, we need to mark it
113 113 as removed.
114 114 """
115 115
116 116 action = []
117 117 state = branchmerge and 'r' or 'f'
118 118 for f in wctx.deleted():
119 119 if f not in mctx:
120 120 action.append((f, state))
121 121
122 122 if not branchmerge:
123 123 for f in wctx.removed():
124 124 if f not in mctx:
125 125 action.append((f, "f"))
126 126
127 127 return action
128 128
129 129 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
130 130 """
131 131 Merge p1 and p2 with ancestor pa and generate merge action list
132 132
133 133 overwrite = whether we clobber working files
134 134 partial = function to filter file lists
135 135 """
136 136
137 137 def fmerge(f, f2, fa):
138 138 """merge flags"""
139 139 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
140 140 if m == n: # flags agree
141 141 return m # unchanged
142 142 if m and n and not a: # flags set, don't agree, differ from parent
143 143 r = repo.ui.promptchoice(
144 144 _(" conflicting flags for %s\n"
145 145 "(n)one, e(x)ec or sym(l)ink?") % f,
146 146 (_("&None"), _("E&xec"), _("Sym&link")), 0)
147 147 if r == 1:
148 148 return "x" # Exec
149 149 if r == 2:
150 150 return "l" # Symlink
151 151 return ""
152 152 if m and m != a: # changed from a to m
153 153 return m
154 154 if n and n != a: # changed from a to n
155 155 return n
156 156 return '' # flag was cleared
157 157
158 158 def act(msg, m, f, *args):
159 159 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
160 160 action.append((f, m) + args)
161 161
162 162 action, copy = [], {}
163 163
164 164 if overwrite:
165 165 pa = p1
166 166 elif pa == p2: # backwards
167 167 pa = p1.p1()
168 168 elif pa and repo.ui.configbool("merge", "followcopies", True):
169 169 dirs = repo.ui.configbool("merge", "followdirs", True)
170 170 copy, diverge = copies.copies(repo, p1, p2, pa, dirs)
171 171 for of, fl in diverge.iteritems():
172 172 act("divergent renames", "dr", of, fl)
173 173
174 174 repo.ui.note(_("resolving manifests\n"))
175 175 repo.ui.debug(" overwrite %s partial %s\n" % (overwrite, bool(partial)))
176 176 repo.ui.debug(" ancestor %s local %s remote %s\n" % (pa, p1, p2))
177 177
178 178 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
179 179 copied = set(copy.values())
180 180
181 181 if '.hgsubstate' in m1:
182 182 # check whether sub state is modified
183 183 for s in p1.substate:
184 184 if p1.sub(s).dirty():
185 185 m1['.hgsubstate'] += "+"
186 186 break
187 187
188 188 # Compare manifests
189 189 for f, n in m1.iteritems():
190 190 if partial and not partial(f):
191 191 continue
192 192 if f in m2:
193 193 rflags = fmerge(f, f, f)
194 194 a = ma.get(f, nullid)
195 195 if n == m2[f] or m2[f] == a: # same or local newer
196 196 # is file locally modified or flags need changing?
197 197 # dirstate flags may need to be made current
198 198 if m1.flags(f) != rflags or n[20:]:
199 199 act("update permissions", "e", f, rflags)
200 200 elif n == a: # remote newer
201 201 act("remote is newer", "g", f, rflags)
202 202 else: # both changed
203 203 act("versions differ", "m", f, f, f, rflags, False)
204 204 elif f in copied: # files we'll deal with on m2 side
205 205 pass
206 206 elif f in copy:
207 207 f2 = copy[f]
208 208 if f2 not in m2: # directory rename
209 209 act("remote renamed directory to " + f2, "d",
210 210 f, None, f2, m1.flags(f))
211 211 else: # case 2 A,B/B/B or case 4,21 A/B/B
212 212 act("local copied/moved to " + f2, "m",
213 213 f, f2, f, fmerge(f, f2, f2), False)
214 214 elif f in ma: # clean, a different, no remote
215 215 if n != ma[f]:
216 216 if repo.ui.promptchoice(
217 217 _(" local changed %s which remote deleted\n"
218 218 "use (c)hanged version or (d)elete?") % f,
219 219 (_("&Changed"), _("&Delete")), 0):
220 220 act("prompt delete", "r", f)
221 221 else:
222 222 act("prompt keep", "a", f)
223 223 elif n[20:] == "a": # added, no remote
224 224 act("remote deleted", "f", f)
225 225 elif n[20:] != "u":
226 226 act("other deleted", "r", f)
227 227
228 228 for f, n in m2.iteritems():
229 229 if partial and not partial(f):
230 230 continue
231 231 if f in m1 or f in copied: # files already visited
232 232 continue
233 233 if f in copy:
234 234 f2 = copy[f]
235 235 if f2 not in m1: # directory rename
236 236 act("local renamed directory to " + f2, "d",
237 237 None, f, f2, m2.flags(f))
238 238 elif f2 in m2: # rename case 1, A/A,B/A
239 239 act("remote copied to " + f, "m",
240 240 f2, f, f, fmerge(f2, f, f2), False)
241 241 else: # case 3,20 A/B/A
242 242 act("remote moved to " + f, "m",
243 243 f2, f, f, fmerge(f2, f, f2), True)
244 244 elif f not in ma:
245 245 act("remote created", "g", f, m2.flags(f))
246 246 elif n != ma[f]:
247 247 if repo.ui.promptchoice(
248 248 _("remote changed %s which local deleted\n"
249 249 "use (c)hanged version or leave (d)eleted?") % f,
250 250 (_("&Changed"), _("&Deleted")), 0) == 0:
251 251 act("prompt recreating", "g", f, m2.flags(f))
252 252
253 253 return action
254 254
255 255 def actionkey(a):
256 256 return a[1] == 'r' and -1 or 0, a
257 257
258 258 def applyupdates(repo, action, wctx, mctx, actx, overwrite):
259 259 """apply the merge action list to the working directory
260 260
261 261 wctx is the working copy context
262 262 mctx is the context to be merged into the working copy
263 263 actx is the context of the common ancestor
264 264
265 265 Return a tuple of counts (updated, merged, removed, unresolved) that
266 266 describes how many files were affected by the update.
267 267 """
268 268
269 269 updated, merged, removed, unresolved = 0, 0, 0, 0
270 270 ms = mergestate(repo)
271 271 ms.reset(wctx.p1().node())
272 272 moves = []
273 273 action.sort(key=actionkey)
274 274
275 275 # prescan for merges
276 276 u = repo.ui
277 277 for a in action:
278 278 f, m = a[:2]
279 279 if m == 'm': # merge
280 280 f2, fd, flags, move = a[2:]
281 281 if f == '.hgsubstate': # merged internally
282 282 continue
283 283 repo.ui.debug("preserving %s for resolve of %s\n" % (f, fd))
284 284 fcl = wctx[f]
285 285 fco = mctx[f2]
286 286 if mctx == actx: # backwards, use working dir parent as ancestor
287 287 if fcl.parents():
288 288 fca = fcl.p1()
289 289 else:
290 290 fca = repo.filectx(f, fileid=nullrev)
291 291 else:
292 292 fca = fcl.ancestor(fco, actx)
293 293 if not fca:
294 294 fca = repo.filectx(f, fileid=nullrev)
295 295 ms.add(fcl, fco, fca, fd, flags)
296 296 if f != fd and move:
297 297 moves.append(f)
298 298
299 audit = scmutil.pathauditor(repo.root)
300
299 301 # remove renamed files after safely stored
300 302 for f in moves:
301 303 if os.path.lexists(repo.wjoin(f)):
302 304 repo.ui.debug("removing %s\n" % f)
305 audit(f)
303 306 os.unlink(repo.wjoin(f))
304 307
305 audit_path = scmutil.pathauditor(repo.root)
306
307 308 numupdates = len(action)
308 309 for i, a in enumerate(action):
309 310 f, m = a[:2]
310 311 u.progress(_('updating'), i + 1, item=f, total=numupdates,
311 312 unit=_('files'))
312 313 if f and f[0] == "/":
313 314 continue
314 315 if m == "r": # remove
315 316 repo.ui.note(_("removing %s\n") % f)
316 audit_path(f)
317 audit(f)
317 318 if f == '.hgsubstate': # subrepo states need updating
318 319 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
319 320 try:
320 321 util.unlinkpath(repo.wjoin(f))
321 322 except OSError, inst:
322 323 if inst.errno != errno.ENOENT:
323 324 repo.ui.warn(_("update failed to remove %s: %s!\n") %
324 325 (f, inst.strerror))
325 326 removed += 1
326 327 elif m == "m": # merge
327 328 if f == '.hgsubstate': # subrepo states need updating
328 329 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx), overwrite)
329 330 continue
330 331 f2, fd, flags, move = a[2:]
331 332 r = ms.resolve(fd, wctx, mctx)
332 333 if r is not None and r > 0:
333 334 unresolved += 1
334 335 else:
335 336 if r is None:
336 337 updated += 1
337 338 else:
338 339 merged += 1
339 340 util.setflags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
340 341 if (move and repo.dirstate.normalize(fd) != f
341 342 and os.path.lexists(repo.wjoin(f))):
342 343 repo.ui.debug("removing %s\n" % f)
344 audit(f)
343 345 os.unlink(repo.wjoin(f))
344 346 elif m == "g": # get
345 347 flags = a[2]
346 348 repo.ui.note(_("getting %s\n") % f)
347 349 t = mctx.filectx(f).data()
348 350 repo.wwrite(f, t, flags)
349 351 t = None
350 352 updated += 1
351 353 if f == '.hgsubstate': # subrepo states need updating
352 354 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
353 355 elif m == "d": # directory rename
354 356 f2, fd, flags = a[2:]
355 357 if f:
356 358 repo.ui.note(_("moving %s to %s\n") % (f, fd))
359 audit(f)
357 360 t = wctx.filectx(f).data()
358 361 repo.wwrite(fd, t, flags)
359 362 util.unlinkpath(repo.wjoin(f))
360 363 if f2:
361 364 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
362 365 t = mctx.filectx(f2).data()
363 366 repo.wwrite(fd, t, flags)
364 367 updated += 1
365 368 elif m == "dr": # divergent renames
366 369 fl = a[2]
367 370 repo.ui.warn(_("note: possible conflict - %s was renamed "
368 371 "multiple times to:\n") % f)
369 372 for nf in fl:
370 373 repo.ui.warn(" %s\n" % nf)
371 374 elif m == "e": # exec
372 375 flags = a[2]
373 376 util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
374 377 ms.commit()
375 378 u.progress(_('updating'), None, total=numupdates, unit=_('files'))
376 379
377 380 return updated, merged, removed, unresolved
378 381
379 382 def recordupdates(repo, action, branchmerge):
380 383 "record merge actions to the dirstate"
381 384
382 385 for a in action:
383 386 f, m = a[:2]
384 387 if m == "r": # remove
385 388 if branchmerge:
386 389 repo.dirstate.remove(f)
387 390 else:
388 391 repo.dirstate.forget(f)
389 392 elif m == "a": # re-add
390 393 if not branchmerge:
391 394 repo.dirstate.add(f)
392 395 elif m == "f": # forget
393 396 repo.dirstate.forget(f)
394 397 elif m == "e": # exec change
395 398 repo.dirstate.normallookup(f)
396 399 elif m == "g": # get
397 400 if branchmerge:
398 401 repo.dirstate.otherparent(f)
399 402 else:
400 403 repo.dirstate.normal(f)
401 404 elif m == "m": # merge
402 405 f2, fd, flag, move = a[2:]
403 406 if branchmerge:
404 407 # We've done a branch merge, mark this file as merged
405 408 # so that we properly record the merger later
406 409 repo.dirstate.merge(fd)
407 410 if f != f2: # copy/rename
408 411 if move:
409 412 repo.dirstate.remove(f)
410 413 if f != fd:
411 414 repo.dirstate.copy(f, fd)
412 415 else:
413 416 repo.dirstate.copy(f2, fd)
414 417 else:
415 418 # We've update-merged a locally modified file, so
416 419 # we set the dirstate to emulate a normal checkout
417 420 # of that file some time in the past. Thus our
418 421 # merge will appear as a normal local file
419 422 # modification.
420 423 if f2 == fd: # file not locally copied/moved
421 424 repo.dirstate.normallookup(fd)
422 425 if move:
423 426 repo.dirstate.forget(f)
424 427 elif m == "d": # directory rename
425 428 f2, fd, flag = a[2:]
426 429 if not f2 and f not in repo.dirstate:
427 430 # untracked file moved
428 431 continue
429 432 if branchmerge:
430 433 repo.dirstate.add(fd)
431 434 if f:
432 435 repo.dirstate.remove(f)
433 436 repo.dirstate.copy(f, fd)
434 437 if f2:
435 438 repo.dirstate.copy(f2, fd)
436 439 else:
437 440 repo.dirstate.normal(fd)
438 441 if f:
439 442 repo.dirstate.forget(f)
440 443
441 444 def update(repo, node, branchmerge, force, partial, ancestor=None):
442 445 """
443 446 Perform a merge between the working directory and the given node
444 447
445 448 node = the node to update to, or None if unspecified
446 449 branchmerge = whether to merge between branches
447 450 force = whether to force branch merging or file overwriting
448 451 partial = a function to filter file lists (dirstate not updated)
449 452
450 453 The table below shows all the behaviors of the update command
451 454 given the -c and -C or no options, whether the working directory
452 455 is dirty, whether a revision is specified, and the relationship of
453 456 the parent rev to the target rev (linear, on the same named
454 457 branch, or on another named branch).
455 458
456 459 This logic is tested by test-update-branches.t.
457 460
458 461 -c -C dirty rev | linear same cross
459 462 n n n n | ok (1) x
460 463 n n n y | ok ok ok
461 464 n n y * | merge (2) (2)
462 465 n y * * | --- discard ---
463 466 y n y * | --- (3) ---
464 467 y n n * | --- ok ---
465 468 y y * * | --- (4) ---
466 469
467 470 x = can't happen
468 471 * = don't-care
469 472 1 = abort: crosses branches (use 'hg merge' or 'hg update -c')
470 473 2 = abort: crosses branches (use 'hg merge' to merge or
471 474 use 'hg update -C' to discard changes)
472 475 3 = abort: uncommitted local changes
473 476 4 = incompatible options (checked in commands.py)
474 477
475 478 Return the same tuple as applyupdates().
476 479 """
477 480
478 481 onode = node
479 482 wlock = repo.wlock()
480 483 try:
481 484 wc = repo[None]
482 485 if node is None:
483 486 # tip of current branch
484 487 try:
485 488 node = repo.branchtags()[wc.branch()]
486 489 except KeyError:
487 490 if wc.branch() == "default": # no default branch!
488 491 node = repo.lookup("tip") # update to tip
489 492 else:
490 493 raise util.Abort(_("branch %s not found") % wc.branch())
491 494 overwrite = force and not branchmerge
492 495 pl = wc.parents()
493 496 p1, p2 = pl[0], repo[node]
494 497 if ancestor:
495 498 pa = repo[ancestor]
496 499 else:
497 500 pa = p1.ancestor(p2)
498 501
499 502 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
500 503
501 504 ### check phase
502 505 if not overwrite and len(pl) > 1:
503 506 raise util.Abort(_("outstanding uncommitted merges"))
504 507 if branchmerge:
505 508 if pa == p2:
506 509 raise util.Abort(_("merging with a working directory ancestor"
507 510 " has no effect"))
508 511 elif pa == p1:
509 512 if p1.branch() == p2.branch():
510 513 raise util.Abort(_("nothing to merge (use 'hg update'"
511 514 " or check 'hg heads')"))
512 515 if not force and (wc.files() or wc.deleted()):
513 516 raise util.Abort(_("outstanding uncommitted changes "
514 517 "(use 'hg status' to list changes)"))
515 518 for s in wc.substate:
516 519 if wc.sub(s).dirty():
517 520 raise util.Abort(_("outstanding uncommitted changes in "
518 521 "subrepository '%s'") % s)
519 522
520 523 elif not overwrite:
521 524 if pa == p1 or pa == p2: # linear
522 525 pass # all good
523 526 elif wc.files() or wc.deleted():
524 527 raise util.Abort(_("crosses branches (merge branches or use"
525 528 " --clean to discard changes)"))
526 529 elif onode is None:
527 530 raise util.Abort(_("crosses branches (merge branches or use"
528 531 " --check to force update)"))
529 532 else:
530 533 # Allow jumping branches if clean and specific rev given
531 534 overwrite = True
532 535
533 536 ### calculate phase
534 537 action = []
535 538 wc.status(unknown=True) # prime cache
536 539 if not force:
537 540 _checkunknown(wc, p2)
538 541 if not util.checkcase(repo.path):
539 542 _checkcollision(p2)
540 543 action += _forgetremoved(wc, p2, branchmerge)
541 544 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
542 545
543 546 ### apply phase
544 547 if not branchmerge: # just jump to the new rev
545 548 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
546 549 if not partial:
547 550 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
548 551
549 552 stats = applyupdates(repo, action, wc, p2, pa, overwrite)
550 553
551 554 if not partial:
552 555 repo.dirstate.setparents(fp1, fp2)
553 556 recordupdates(repo, action, branchmerge)
554 557 if not branchmerge:
555 558 repo.dirstate.setbranch(p2.branch())
556 559 finally:
557 560 wlock.release()
558 561
559 562 if not partial:
560 563 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
561 564 return stats
General Comments 0
You need to be logged in to leave comments. Login now