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