##// END OF EJS Templates
applyupdates: audit path on flag changes...
Adrian Buehlmann -
r14405:f2295a82 default
parent child Browse files
Show More
@@ -1,564 +1,565
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 299 audit = scmutil.pathauditor(repo.root)
300 300
301 301 # remove renamed files after safely stored
302 302 for f in moves:
303 303 if os.path.lexists(repo.wjoin(f)):
304 304 repo.ui.debug("removing %s\n" % f)
305 305 audit(f)
306 306 os.unlink(repo.wjoin(f))
307 307
308 308 numupdates = len(action)
309 309 for i, a in enumerate(action):
310 310 f, m = a[:2]
311 311 u.progress(_('updating'), i + 1, item=f, total=numupdates,
312 312 unit=_('files'))
313 313 if f and f[0] == "/":
314 314 continue
315 315 if m == "r": # remove
316 316 repo.ui.note(_("removing %s\n") % f)
317 317 audit(f)
318 318 if f == '.hgsubstate': # subrepo states need updating
319 319 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
320 320 try:
321 321 util.unlinkpath(repo.wjoin(f))
322 322 except OSError, inst:
323 323 if inst.errno != errno.ENOENT:
324 324 repo.ui.warn(_("update failed to remove %s: %s!\n") %
325 325 (f, inst.strerror))
326 326 removed += 1
327 327 elif m == "m": # merge
328 328 if f == '.hgsubstate': # subrepo states need updating
329 329 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx), overwrite)
330 330 continue
331 331 f2, fd, flags, move = a[2:]
332 332 r = ms.resolve(fd, wctx, mctx)
333 333 if r is not None and r > 0:
334 334 unresolved += 1
335 335 else:
336 336 if r is None:
337 337 updated += 1
338 338 else:
339 339 merged += 1
340 340 util.setflags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
341 341 if (move and repo.dirstate.normalize(fd) != f
342 342 and os.path.lexists(repo.wjoin(f))):
343 343 repo.ui.debug("removing %s\n" % f)
344 344 audit(f)
345 345 os.unlink(repo.wjoin(f))
346 346 elif m == "g": # get
347 347 flags = a[2]
348 348 repo.ui.note(_("getting %s\n") % f)
349 349 t = mctx.filectx(f).data()
350 350 repo.wwrite(f, t, flags)
351 351 t = None
352 352 updated += 1
353 353 if f == '.hgsubstate': # subrepo states need updating
354 354 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
355 355 elif m == "d": # directory rename
356 356 f2, fd, flags = a[2:]
357 357 if f:
358 358 repo.ui.note(_("moving %s to %s\n") % (f, fd))
359 359 audit(f)
360 360 t = wctx.filectx(f).data()
361 361 repo.wwrite(fd, t, flags)
362 362 util.unlinkpath(repo.wjoin(f))
363 363 if f2:
364 364 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
365 365 t = mctx.filectx(f2).data()
366 366 repo.wwrite(fd, t, flags)
367 367 updated += 1
368 368 elif m == "dr": # divergent renames
369 369 fl = a[2]
370 370 repo.ui.warn(_("note: possible conflict - %s was renamed "
371 371 "multiple times to:\n") % f)
372 372 for nf in fl:
373 373 repo.ui.warn(" %s\n" % nf)
374 374 elif m == "e": # exec
375 375 flags = a[2]
376 repo.wopener.audit(f)
376 377 util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
377 378 ms.commit()
378 379 u.progress(_('updating'), None, total=numupdates, unit=_('files'))
379 380
380 381 return updated, merged, removed, unresolved
381 382
382 383 def recordupdates(repo, action, branchmerge):
383 384 "record merge actions to the dirstate"
384 385
385 386 for a in action:
386 387 f, m = a[:2]
387 388 if m == "r": # remove
388 389 if branchmerge:
389 390 repo.dirstate.remove(f)
390 391 else:
391 392 repo.dirstate.forget(f)
392 393 elif m == "a": # re-add
393 394 if not branchmerge:
394 395 repo.dirstate.add(f)
395 396 elif m == "f": # forget
396 397 repo.dirstate.forget(f)
397 398 elif m == "e": # exec change
398 399 repo.dirstate.normallookup(f)
399 400 elif m == "g": # get
400 401 if branchmerge:
401 402 repo.dirstate.otherparent(f)
402 403 else:
403 404 repo.dirstate.normal(f)
404 405 elif m == "m": # merge
405 406 f2, fd, flag, move = a[2:]
406 407 if branchmerge:
407 408 # We've done a branch merge, mark this file as merged
408 409 # so that we properly record the merger later
409 410 repo.dirstate.merge(fd)
410 411 if f != f2: # copy/rename
411 412 if move:
412 413 repo.dirstate.remove(f)
413 414 if f != fd:
414 415 repo.dirstate.copy(f, fd)
415 416 else:
416 417 repo.dirstate.copy(f2, fd)
417 418 else:
418 419 # We've update-merged a locally modified file, so
419 420 # we set the dirstate to emulate a normal checkout
420 421 # of that file some time in the past. Thus our
421 422 # merge will appear as a normal local file
422 423 # modification.
423 424 if f2 == fd: # file not locally copied/moved
424 425 repo.dirstate.normallookup(fd)
425 426 if move:
426 427 repo.dirstate.forget(f)
427 428 elif m == "d": # directory rename
428 429 f2, fd, flag = a[2:]
429 430 if not f2 and f not in repo.dirstate:
430 431 # untracked file moved
431 432 continue
432 433 if branchmerge:
433 434 repo.dirstate.add(fd)
434 435 if f:
435 436 repo.dirstate.remove(f)
436 437 repo.dirstate.copy(f, fd)
437 438 if f2:
438 439 repo.dirstate.copy(f2, fd)
439 440 else:
440 441 repo.dirstate.normal(fd)
441 442 if f:
442 443 repo.dirstate.forget(f)
443 444
444 445 def update(repo, node, branchmerge, force, partial, ancestor=None):
445 446 """
446 447 Perform a merge between the working directory and the given node
447 448
448 449 node = the node to update to, or None if unspecified
449 450 branchmerge = whether to merge between branches
450 451 force = whether to force branch merging or file overwriting
451 452 partial = a function to filter file lists (dirstate not updated)
452 453
453 454 The table below shows all the behaviors of the update command
454 455 given the -c and -C or no options, whether the working directory
455 456 is dirty, whether a revision is specified, and the relationship of
456 457 the parent rev to the target rev (linear, on the same named
457 458 branch, or on another named branch).
458 459
459 460 This logic is tested by test-update-branches.t.
460 461
461 462 -c -C dirty rev | linear same cross
462 463 n n n n | ok (1) x
463 464 n n n y | ok ok ok
464 465 n n y * | merge (2) (2)
465 466 n y * * | --- discard ---
466 467 y n y * | --- (3) ---
467 468 y n n * | --- ok ---
468 469 y y * * | --- (4) ---
469 470
470 471 x = can't happen
471 472 * = don't-care
472 473 1 = abort: crosses branches (use 'hg merge' or 'hg update -c')
473 474 2 = abort: crosses branches (use 'hg merge' to merge or
474 475 use 'hg update -C' to discard changes)
475 476 3 = abort: uncommitted local changes
476 477 4 = incompatible options (checked in commands.py)
477 478
478 479 Return the same tuple as applyupdates().
479 480 """
480 481
481 482 onode = node
482 483 wlock = repo.wlock()
483 484 try:
484 485 wc = repo[None]
485 486 if node is None:
486 487 # tip of current branch
487 488 try:
488 489 node = repo.branchtags()[wc.branch()]
489 490 except KeyError:
490 491 if wc.branch() == "default": # no default branch!
491 492 node = repo.lookup("tip") # update to tip
492 493 else:
493 494 raise util.Abort(_("branch %s not found") % wc.branch())
494 495 overwrite = force and not branchmerge
495 496 pl = wc.parents()
496 497 p1, p2 = pl[0], repo[node]
497 498 if ancestor:
498 499 pa = repo[ancestor]
499 500 else:
500 501 pa = p1.ancestor(p2)
501 502
502 503 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
503 504
504 505 ### check phase
505 506 if not overwrite and len(pl) > 1:
506 507 raise util.Abort(_("outstanding uncommitted merges"))
507 508 if branchmerge:
508 509 if pa == p2:
509 510 raise util.Abort(_("merging with a working directory ancestor"
510 511 " has no effect"))
511 512 elif pa == p1:
512 513 if p1.branch() == p2.branch():
513 514 raise util.Abort(_("nothing to merge (use 'hg update'"
514 515 " or check 'hg heads')"))
515 516 if not force and (wc.files() or wc.deleted()):
516 517 raise util.Abort(_("outstanding uncommitted changes "
517 518 "(use 'hg status' to list changes)"))
518 519 for s in wc.substate:
519 520 if wc.sub(s).dirty():
520 521 raise util.Abort(_("outstanding uncommitted changes in "
521 522 "subrepository '%s'") % s)
522 523
523 524 elif not overwrite:
524 525 if pa == p1 or pa == p2: # linear
525 526 pass # all good
526 527 elif wc.files() or wc.deleted():
527 528 raise util.Abort(_("crosses branches (merge branches or use"
528 529 " --clean to discard changes)"))
529 530 elif onode is None:
530 531 raise util.Abort(_("crosses branches (merge branches or use"
531 532 " --check to force update)"))
532 533 else:
533 534 # Allow jumping branches if clean and specific rev given
534 535 overwrite = True
535 536
536 537 ### calculate phase
537 538 action = []
538 539 wc.status(unknown=True) # prime cache
539 540 if not force:
540 541 _checkunknown(wc, p2)
541 542 if not util.checkcase(repo.path):
542 543 _checkcollision(p2)
543 544 action += _forgetremoved(wc, p2, branchmerge)
544 545 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
545 546
546 547 ### apply phase
547 548 if not branchmerge: # just jump to the new rev
548 549 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
549 550 if not partial:
550 551 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
551 552
552 553 stats = applyupdates(repo, action, wc, p2, pa, overwrite)
553 554
554 555 if not partial:
555 556 repo.dirstate.setparents(fp1, fp2)
556 557 recordupdates(repo, action, branchmerge)
557 558 if not branchmerge:
558 559 repo.dirstate.setbranch(p2.branch())
559 560 finally:
560 561 wlock.release()
561 562
562 563 if not partial:
563 564 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
564 565 return stats
General Comments 0
You need to be logged in to leave comments. Login now