comparison mercurial/merge.py @ 19105:c60a7f5a741f stable

icasefs: rewrite case-folding collision detection (issue3452) Before this patch, case-folding collision detection uses "copies.pathcopies()" before "manifestmerge()", and is not aware of renaming in some cases. For example, in the case of issue3452, "copies.pathcopies()" can't detect renaming, if the file is renamed at the revision before common ancestor of merging. So, "hg merge" is aborted unexpectedly on case insensitive filesystem. This patch fully rewrites case-folding collision detection, and relocate it into "manifestmerge()". New implementation uses list of actions held in "actions" and "prompts" to build provisional merged manifest up. Provisional merged manifest should be correct, if actions required to build merge result up in working directory are listed up in "actions" and "prompts" correctly. This patch checks case-folding collision still before prompting for merge, to avoid aborting after some interactions with users. So, this assumes that user would choose not "deleted" but "changed". This patch also changes existing abort message, because sorting before collision detection changes order of checked files.
author FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
date Tue, 30 Apr 2013 05:01:32 +0900
parents 5cc71484ee9c
children 113681bbef9e
comparison
equal deleted inserted replaced
19104:370d9ea027b1 19105:c60a7f5a741f
108 wctx._repo.ui.warn(_("%s: untracked file differs\n") % f) 108 wctx._repo.ui.warn(_("%s: untracked file differs\n") % f)
109 if error: 109 if error:
110 raise util.Abort(_("untracked files in working directory differ " 110 raise util.Abort(_("untracked files in working directory differ "
111 "from files in requested revision")) 111 "from files in requested revision"))
112 112
113 def _remains(f, m, ma, workingctx=False):
114 """check whether specified file remains after merge.
115
116 It is assumed that specified file is not contained in the manifest
117 of the other context.
118 """
119 if f in ma:
120 n = m[f]
121 if n != ma[f]:
122 return True # because it is changed locally
123 # even though it doesn't remain, if "remote deleted" is
124 # chosen in manifestmerge()
125 elif workingctx and n[20:] == "a":
126 return True # because it is added locally (linear merge specific)
127 else:
128 return False # because it is removed remotely
129 else:
130 return True # because it is added locally
131
132 def _checkcollision(mctx, extractxs):
133 "check for case folding collisions in the destination context"
134 folded = {}
135 for fn in mctx:
136 fold = util.normcase(fn)
137 if fold in folded:
138 raise util.Abort(_("case-folding collision between %s and %s")
139 % (fn, folded[fold]))
140 folded[fold] = fn
141
142 if extractxs:
143 wctx, actx = extractxs
144 # class to delay looking up copy mapping
145 class pathcopies(object):
146 @util.propertycache
147 def map(self):
148 # {dst@mctx: src@wctx} copy mapping
149 return copies.pathcopies(wctx, mctx)
150 pc = pathcopies()
151
152 for fn in wctx:
153 fold = util.normcase(fn)
154 mfn = folded.get(fold, None)
155 if (mfn and mfn != fn and pc.map.get(mfn) != fn and
156 _remains(fn, wctx.manifest(), actx.manifest(), True) and
157 _remains(mfn, mctx.manifest(), actx.manifest())):
158 raise util.Abort(_("case-folding collision between %s and %s")
159 % (mfn, fn))
160
161 def _forgetremoved(wctx, mctx, branchmerge): 113 def _forgetremoved(wctx, mctx, branchmerge):
162 """ 114 """
163 Forget removed files 115 Forget removed files
164 116
165 If we're jumping between revisions (as opposed to merging), and if 117 If we're jumping between revisions (as opposed to merging), and if
183 for f in wctx.removed(): 135 for f in wctx.removed():
184 if f not in mctx: 136 if f not in mctx:
185 actions.append((f, "f", None, "forget removed")) 137 actions.append((f, "f", None, "forget removed"))
186 138
187 return actions 139 return actions
140
141 def _checkcollision(repo, wmf, actions, prompts):
142 # build provisional merged manifest up
143 pmmf = set(wmf)
144
145 def addop(f, args):
146 pmmf.add(f)
147 def removeop(f, args):
148 pmmf.discard(f)
149 def nop(f, args):
150 pass
151
152 def renameop(f, args):
153 f2, fd, flags = args
154 if f:
155 pmmf.discard(f)
156 pmmf.add(fd)
157 def mergeop(f, args):
158 f2, fd, move = args
159 if move:
160 pmmf.discard(f)
161 pmmf.add(fd)
162
163 opmap = {
164 "a": addop,
165 "d": renameop,
166 "dr": nop,
167 "e": nop,
168 "f": addop, # untracked file should be kept in working directory
169 "g": addop,
170 "m": mergeop,
171 "r": removeop,
172 "rd": nop,
173 }
174 for f, m, args, msg in actions:
175 op = opmap.get(m)
176 assert op, m
177 op(f, args)
178
179 opmap = {
180 "cd": addop,
181 "dc": addop,
182 }
183 for f, m in prompts:
184 op = opmap.get(m)
185 assert op, m
186 op(f, None)
187
188 # check case-folding collision in provisional merged manifest
189 foldmap = {}
190 for f in sorted(pmmf):
191 fold = util.normcase(f)
192 if fold in foldmap:
193 raise util.Abort(_("case-folding collision between %s and %s")
194 % (f, foldmap[fold]))
195 foldmap[fold] = f
188 196
189 def manifestmerge(repo, wctx, p2, pa, branchmerge, force, partial, 197 def manifestmerge(repo, wctx, p2, pa, branchmerge, force, partial,
190 acceptremote=False): 198 acceptremote=False):
191 """ 199 """
192 Merge p1 and p2 with ancestor pa and generate merge action list 200 Merge p1 and p2 with ancestor pa and generate merge action list
340 else: assert False, m 348 else: assert False, m
341 if aborts: 349 if aborts:
342 raise util.Abort(_("untracked files in working directory differ " 350 raise util.Abort(_("untracked files in working directory differ "
343 "from files in requested revision")) 351 "from files in requested revision"))
344 352
353 if not util.checkcase(repo.path):
354 # check collision between files only in p2 for clean update
355 if (not branchmerge and
356 (force or not wctx.dirty(missing=True, branch=False))):
357 _checkcollision(repo, m2, [], [])
358 else:
359 _checkcollision(repo, m1, actions, prompts)
360
345 for f, m in sorted(prompts): 361 for f, m in sorted(prompts):
346 if m == "cd": 362 if m == "cd":
347 if acceptremote: 363 if acceptremote:
348 actions.append((f, "r", None, "remote delete")) 364 actions.append((f, "r", None, "remote delete"))
349 elif repo.ui.promptchoice( 365 elif repo.ui.promptchoice(
539 555
540 def calculateupdates(repo, tctx, mctx, ancestor, branchmerge, force, partial, 556 def calculateupdates(repo, tctx, mctx, ancestor, branchmerge, force, partial,
541 acceptremote=False): 557 acceptremote=False):
542 "Calculate the actions needed to merge mctx into tctx" 558 "Calculate the actions needed to merge mctx into tctx"
543 actions = [] 559 actions = []
544 folding = not util.checkcase(repo.path)
545 if folding:
546 # collision check is not needed for clean update
547 if (not branchmerge and
548 (force or not tctx.dirty(missing=True, branch=False))):
549 _checkcollision(mctx, None)
550 else:
551 _checkcollision(mctx, (tctx, ancestor))
552 actions += manifestmerge(repo, tctx, mctx, 560 actions += manifestmerge(repo, tctx, mctx,
553 ancestor, 561 ancestor,
554 branchmerge, force, 562 branchmerge, force,
555 partial, acceptremote) 563 partial, acceptremote)
556 if tctx.rev() is None: 564 if tctx.rev() is None: