Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/merge.py @ 18338:384df4db6520
merge: merge file flags together with file content
The 'x' flag and the 'l' flag are very different. It is usually not a problem
to change the 'x' flag of a normal file independent of the content, but one
does not simply change the type of a file to 'l' independent of the content.
This removes the fmerge function that merged both 'x' and 'l' independent of
content early in the merge process. This correctly introduces some conflicts
instead of silent incorrect merges. 3-way flag merge will now be done in the
resolve process, right next to file content merge. Conflicts can thus be
resolved with (slightly inconvenient) resolve commands like 'resolve f --tool
internal:other'. It thus brings us closer to be able to re-solve manifest merge
after the merge and avoid prompts during merge.
This also removes the "conflicting flags for a - (n)one, e(x)ec or sym(l)ink?"
prompt that nobody could answer and that made it easy to mix symlink targets
and file contents up. Instead it will give a file merge where a sufficiently
clever merge tool can help resolving the issue.
author | Mads Kiilerich <mads@kiilerich.com> |
---|---|
date | Wed, 09 Jan 2013 02:02:45 +0100 |
parents | 77973b6a7b0b |
children | aadefcee1f5e |
comparison
equal
deleted
inserted
replaced
18337:557c8522aec0 | 18338:384df4db6520 |
---|---|
43 f.write(hex(self._local) + "\n") | 43 f.write(hex(self._local) + "\n") |
44 for d, v in self._state.iteritems(): | 44 for d, v in self._state.iteritems(): |
45 f.write("\0".join([d] + v) + "\n") | 45 f.write("\0".join([d] + v) + "\n") |
46 f.close() | 46 f.close() |
47 self._dirty = False | 47 self._dirty = False |
48 def add(self, fcl, fco, fca, fd, flags): | 48 def add(self, fcl, fco, fca, fd): |
49 hash = util.sha1(fcl.path()).hexdigest() | 49 hash = util.sha1(fcl.path()).hexdigest() |
50 self._repo.opener.write("merge/" + hash, fcl.data()) | 50 self._repo.opener.write("merge/" + hash, fcl.data()) |
51 self._state[fd] = ['u', hash, fcl.path(), fca.path(), | 51 self._state[fd] = ['u', hash, fcl.path(), fca.path(), |
52 hex(fca.filenode()), fco.path(), flags] | 52 hex(fca.filenode()), fco.path(), fcl.flags()] |
53 self._dirty = True | 53 self._dirty = True |
54 def __contains__(self, dfile): | 54 def __contains__(self, dfile): |
55 return dfile in self._state | 55 return dfile in self._state |
56 def __getitem__(self, dfile): | 56 def __getitem__(self, dfile): |
57 return self._state[dfile][0] | 57 return self._state[dfile][0] |
65 self._dirty = True | 65 self._dirty = True |
66 def resolve(self, dfile, wctx, octx): | 66 def resolve(self, dfile, wctx, octx): |
67 if self[dfile] == 'r': | 67 if self[dfile] == 'r': |
68 return 0 | 68 return 0 |
69 state, hash, lfile, afile, anode, ofile, flags = self._state[dfile] | 69 state, hash, lfile, afile, anode, ofile, flags = self._state[dfile] |
70 fcd = wctx[dfile] | |
71 fco = octx[ofile] | |
72 fca = self._repo.filectx(afile, fileid=anode) | |
73 # "premerge" x flags | |
74 flo = fco.flags() | |
75 fla = fca.flags() | |
76 if 'x' in flags + flo + fla and 'l' not in flags + flo + fla: | |
77 if fca.node() == nullid: | |
78 self._repo.ui.warn(_('warning: cannot merge flags for %s\n') % | |
79 afile) | |
80 elif flags == fla: | |
81 flags = flo | |
82 # restore local | |
70 f = self._repo.opener("merge/" + hash) | 83 f = self._repo.opener("merge/" + hash) |
71 self._repo.wwrite(dfile, f.read(), flags) | 84 self._repo.wwrite(dfile, f.read(), flags) |
72 f.close() | 85 f.close() |
73 fcd = wctx[dfile] | |
74 fco = octx[ofile] | |
75 fca = self._repo.filectx(afile, fileid=anode) | |
76 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca) | 86 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca) |
77 if r is None: | 87 if r is None: |
78 # no real conflict | 88 # no real conflict |
79 del self._state[dfile] | 89 del self._state[dfile] |
80 elif not r: | 90 elif not r: |
181 | 191 |
182 overwrite = whether we clobber working files | 192 overwrite = whether we clobber working files |
183 partial = function to filter file lists | 193 partial = function to filter file lists |
184 """ | 194 """ |
185 | 195 |
186 def fmerge(f, f2, fa): | |
187 """merge flags""" | |
188 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2) | |
189 if m == n: # flags agree | |
190 return m # unchanged | |
191 if m and n and not a: # flags set, don't agree, differ from parent | |
192 r = repo.ui.promptchoice( | |
193 _(" conflicting flags for %s\n" | |
194 "(n)one, e(x)ec or sym(l)ink?") % f, | |
195 (_("&None"), _("E&xec"), _("Sym&link")), 0) | |
196 if r == 1: | |
197 return "x" # Exec | |
198 if r == 2: | |
199 return "l" # Symlink | |
200 return "" | |
201 if m and m != a: # changed from a to m | |
202 return m | |
203 if n and n != a: # changed from a to n | |
204 if (n == 'l' or a == 'l') and m1.get(f) != ma.get(f): | |
205 # can't automatically merge symlink flag when there | |
206 # are file-level conflicts here, let filemerge take | |
207 # care of it | |
208 return m | |
209 return n | |
210 return '' # flag was cleared | |
211 | |
212 def act(msg, m, f, *args): | 196 def act(msg, m, f, *args): |
213 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m)) | 197 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m)) |
214 actions.append((f, m) + args) | 198 actions.append((f, m) + args) |
215 | 199 |
216 actions, copy, movewithdir = [], {}, {} | 200 actions, copy, movewithdir = [], {}, {} |
246 # Compare manifests | 230 # Compare manifests |
247 for f, n in m1.iteritems(): | 231 for f, n in m1.iteritems(): |
248 if partial and not partial(f): | 232 if partial and not partial(f): |
249 continue | 233 continue |
250 if f in m2: | 234 if f in m2: |
251 rflags = fmerge(f, f, f) | 235 n2 = m2[f] |
236 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f) | |
237 nol = 'l' not in fl1 + fl2 + fla | |
252 a = ma.get(f, nullid) | 238 a = ma.get(f, nullid) |
253 if n == m2[f] or m2[f] == a: # same or local newer | 239 if n == n2 and fl1 == fl2: |
254 # is file locally modified or flags need changing? | 240 pass # same - keep local |
255 # dirstate flags may need to be made current | 241 elif n2 == a and fl2 == fla: |
256 if m1.flags(f) != rflags or n[20:]: | 242 pass # remote unchanged - keep local |
257 act("update permissions", "e", f, rflags) | 243 elif n == a and fl1 == fla: # local unchanged - use remote |
258 elif n == a: # remote newer | 244 if n == n2: # optimization: keep local content |
259 act("remote is newer", "g", f, rflags) | 245 act("update permissions", "e", f, fl2) |
260 else: # both changed | 246 else: |
261 act("versions differ", "m", f, f, f, rflags, False) | 247 act("remote is newer", "g", f, fl2) |
248 elif nol and n2 == a: # remote only changed 'x' | |
249 act("update permissions", "e", f, fl2) | |
250 elif nol and n == a: # local only changed 'x' | |
251 act("remote is newer", "g", f, fl) | |
252 else: # both changed something | |
253 act("versions differ", "m", f, f, f, False) | |
262 elif f in copied: # files we'll deal with on m2 side | 254 elif f in copied: # files we'll deal with on m2 side |
263 pass | 255 pass |
264 elif f in movewithdir: # directory rename | 256 elif f in movewithdir: # directory rename |
265 f2 = movewithdir[f] | 257 f2 = movewithdir[f] |
266 act("remote renamed directory to " + f2, "d", f, None, f2, | 258 act("remote renamed directory to " + f2, "d", f, None, f2, |
267 m1.flags(f)) | 259 m1.flags(f)) |
268 elif f in copy: # case 2 A,B/B/B or case 4,21 A/B/B | 260 elif f in copy: # case 2 A,B/B/B or case 4,21 A/B/B |
269 f2 = copy[f] | 261 f2 = copy[f] |
270 act("local copied/moved to " + f2, "m", f, f2, f, | 262 act("local copied/moved to " + f2, "m", f, f2, f, False) |
271 fmerge(f, f2, f2), False) | |
272 elif f in ma: # clean, a different, no remote | 263 elif f in ma: # clean, a different, no remote |
273 if n != ma[f]: | 264 if n != ma[f]: |
274 if repo.ui.promptchoice( | 265 if repo.ui.promptchoice( |
275 _(" local changed %s which remote deleted\n" | 266 _(" local changed %s which remote deleted\n" |
276 "use (c)hanged version or (d)elete?") % f, | 267 "use (c)hanged version or (d)elete?") % f, |
294 m2.flags(f)) | 285 m2.flags(f)) |
295 elif f in copy: | 286 elif f in copy: |
296 f2 = copy[f] | 287 f2 = copy[f] |
297 if f2 in m2: # rename case 1, A/A,B/A | 288 if f2 in m2: # rename case 1, A/A,B/A |
298 act("remote copied to " + f, "m", | 289 act("remote copied to " + f, "m", |
299 f2, f, f, fmerge(f2, f, f2), False) | 290 f2, f, f, False) |
300 else: # case 3,20 A/B/A | 291 else: # case 3,20 A/B/A |
301 act("remote moved to " + f, "m", | 292 act("remote moved to " + f, "m", |
302 f2, f, f, fmerge(f2, f, f2), True) | 293 f2, f, f, True) |
303 elif f not in ma: | 294 elif f not in ma: |
304 if (not overwrite | 295 if (not overwrite |
305 and _checkunknownfile(repo, p1, p2, f)): | 296 and _checkunknownfile(repo, p1, p2, f)): |
306 rflags = fmerge(f, f, f) | |
307 act("remote differs from untracked local", | 297 act("remote differs from untracked local", |
308 "m", f, f, f, rflags, False) | 298 "m", f, f, f, False) |
309 else: | 299 else: |
310 act("remote created", "g", f, m2.flags(f)) | 300 act("remote created", "g", f, m2.flags(f)) |
311 elif n != ma[f]: | 301 elif n != ma[f]: |
312 if repo.ui.promptchoice( | 302 if repo.ui.promptchoice( |
313 _("remote changed %s which local deleted\n" | 303 _("remote changed %s which local deleted\n" |
339 | 329 |
340 # prescan for merges | 330 # prescan for merges |
341 for a in actions: | 331 for a in actions: |
342 f, m = a[:2] | 332 f, m = a[:2] |
343 if m == "m": # merge | 333 if m == "m": # merge |
344 f2, fd, flags, move = a[2:] | 334 f2, fd, move = a[2:] |
345 if fd == '.hgsubstate': # merged internally | 335 if fd == '.hgsubstate': # merged internally |
346 continue | 336 continue |
347 repo.ui.debug("preserving %s for resolve of %s\n" % (f, fd)) | 337 repo.ui.debug("preserving %s for resolve of %s\n" % (f, fd)) |
348 fcl = wctx[f] | 338 fcl = wctx[f] |
349 fco = mctx[f2] | 339 fco = mctx[f2] |
354 fca = repo.filectx(f, fileid=nullrev) | 344 fca = repo.filectx(f, fileid=nullrev) |
355 else: | 345 else: |
356 fca = fcl.ancestor(fco, actx) | 346 fca = fcl.ancestor(fco, actx) |
357 if not fca: | 347 if not fca: |
358 fca = repo.filectx(f, fileid=nullrev) | 348 fca = repo.filectx(f, fileid=nullrev) |
359 ms.add(fcl, fco, fca, fd, flags) | 349 ms.add(fcl, fco, fca, fd) |
360 if f != fd and move: | 350 if f != fd and move: |
361 moves.append(f) | 351 moves.append(f) |
362 | 352 |
363 audit = repo.wopener.audit | 353 audit = repo.wopener.audit |
364 | 354 |
388 elif m == "m": # merge | 378 elif m == "m": # merge |
389 if fd == '.hgsubstate': # subrepo states need updating | 379 if fd == '.hgsubstate': # subrepo states need updating |
390 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx), | 380 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx), |
391 overwrite) | 381 overwrite) |
392 continue | 382 continue |
393 f2, fd, flags, move = a[2:] | 383 f2, fd, move = a[2:] |
394 audit(fd) | 384 audit(fd) |
395 r = ms.resolve(fd, wctx, mctx) | 385 r = ms.resolve(fd, wctx, mctx) |
396 if r is not None and r > 0: | 386 if r is not None and r > 0: |
397 unresolved += 1 | 387 unresolved += 1 |
398 else: | 388 else: |
482 if branchmerge: | 472 if branchmerge: |
483 repo.dirstate.otherparent(f) | 473 repo.dirstate.otherparent(f) |
484 else: | 474 else: |
485 repo.dirstate.normal(f) | 475 repo.dirstate.normal(f) |
486 elif m == "m": # merge | 476 elif m == "m": # merge |
487 f2, fd, flag, move = a[2:] | 477 f2, fd, move = a[2:] |
488 if branchmerge: | 478 if branchmerge: |
489 # We've done a branch merge, mark this file as merged | 479 # We've done a branch merge, mark this file as merged |
490 # so that we properly record the merger later | 480 # so that we properly record the merger later |
491 repo.dirstate.merge(fd) | 481 repo.dirstate.merge(fd) |
492 if f != f2: # copy/rename | 482 if f != f2: # copy/rename |