[PATCH] detect conflicts between merged remote files and directories (issue29)

Evgeniy Makeev evgeniym at fb.com
Tue Oct 23 12:09:30 CDT 2012


# HG changeset patch
# User Evgeniy Makeev <evgeniym at fb.com>
# Date 1350964267 25200
# Node ID 395b8469087e74e0ed98487217c9259e215ec479
# Parent  d51364b318eab1871af13f30be099799c04a43d1
detect conflicts between merged remote files and directories (issue29)

The fix checks for three kinds of conflicts:
  1. Remote file conflicts with a local directory with identical name
    In this case user prompted to abort the merge (default) or to resolve
    the conflict by deleting the remote file
  2. Remote directory conflicts with a local file with the same name
    In this case user prompted to abort (default) or to resolve the conflict
    by deleting all remote files which have the directory in their paths
  3. Remote file conflicts with local empty directory
    This conflict is caught only during applyupdates when the IO already
    in progress. User prompted to resolve conflict by removing the empty
    local directory (default) or abort the merge

diff -r d51364b318ea -r 395b8469087e mercurial/context.py
--- a/mercurial/context.py	Thu Jul 26 21:29:39 2012 +0200
+++ b/mercurial/context.py	Mon Oct 22 20:51:07 2012 -0700
@@ -352,6 +352,23 @@
     def dirs(self):
         return self._dirs
 
+    @propertycache
+    def _normdirs(self):
+        dirs = set()
+        for f in self._manifest:
+            pos = f.rfind('/')
+            while pos != -1:
+                f = f[:pos]
+                fnorm = util.normcase(f)
+                if fnorm in dirs:
+                    break # dirs already contains this and above
+                dirs.add(fnorm)
+                pos = f.rfind('/')
+        return dirs
+
+    def normdirs(self):
+        return self._normdirs
+
 class filectx(object):
     """A filecontext object makes access to data related to a particular
        filerevision convenient."""
diff -r d51364b318ea -r 395b8469087e mercurial/merge.py
--- a/mercurial/merge.py	Thu Jul 26 21:29:39 2012 +0200
+++ b/mercurial/merge.py	Mon Oct 22 20:51:07 2012 -0700
@@ -190,6 +190,31 @@
     def act(msg, m, f, *args):
         repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
         action.append((f, m) + args)
+    
+    def addremotedir(f):
+        # find all dirs from f's path
+        pathpos = f.rfind('/')
+        while pathpos != -1:
+            f = f[:pathpos]
+            if f in newdirset:
+                break # newdirset already contains this and above
+            newdirset.add(f)
+            pathpos = f.rfind('/')
+
+    if util.checkcase(repo.path):
+        def findirs(f):
+            return f in p1.dirs()
+        def addnormdir(fdir):
+            addremotedir(fdir)
+        def finnewdirs(f):
+            return f in newdirset
+    else:
+        def findirs(f):
+            return util.normcase(f) in p1.normdirs()
+        def addnormdir(fdir):
+            addremotedir(util.normcase(fdir))
+        def finnewdirs(f):
+            return util.normcase(f) in newdirset
 
     action, copy = [], {}
 
@@ -259,6 +284,7 @@
             else:
                 act("other deleted", "r", f)
 
+    newdirset = set()
     for f, n in m2.iteritems():
         if partial and not partial(f):
             continue
@@ -282,7 +308,20 @@
                 act("remote differs from untracked local",
                     "m", f, f, f, rflags, False)
             else:
-                act("remote created", "g", f, m2.flags(f))
+                if findirs(f) and os.path.isdir(repo.wjoin(f)):
+                    # check if the added remote file collides with existing dir
+                    if 0 == repo.ui.promptchoice(
+                            _(" remote %s conflicts with local directory %s\n"
+                              "(a)bort operation or (d)elete remote file? [a]")
+                                                 % (f, repo.wjoin(f)),
+                                (_("&Abort"), _("&Delete")), 0):
+                        raise util.Abort(
+                            _("remote %s conflicts with local"
+                              " directory %s") % (f, repo.wjoin(f)))
+                else:
+                    act("remote created", "g", f, m2.flags(f))
+                    addnormdir(f)
+
         elif n != ma[f]:
             if repo.ui.promptchoice(
                 _("remote changed %s which local deleted\n"
@@ -290,6 +329,33 @@
                 (_("&Changed"), _("&Deleted")), 0) == 0:
                 act("prompt recreating", "g", f, m2.flags(f))
 
+    for rfile in m1:
+        # verify if a remote dirs conflicts with an existing file
+        # everything inside the following if block only executes on error
+        if finnewdirs(rfile) and os.path.isfile(repo.wjoin(rfile)):
+            # collission - expand error message/recovery option
+            rdir = util.normcase(rfile) + '/'
+            confiles = [fn for fn in p2 if util.normcase(fn).find(rdir) == 0]
+            confilesstr = ', '.join(confiles)
+            if len(confiles) <= 1:
+                promptmsg = _(" remote %s conflicts with local %s path\n"
+                              "(a)bort or (d)elete remote file %s? [a]")
+                abortmsg = _("local file %s conflicts with path of remote"
+                             " file %s")
+            else:
+                promptmsg = _(" remote %s conflict with local %s path\n"
+                            "(a)bort or (d)elete remote files %s? [a]")
+                abortmsg = _("local file %s conflicts with paths of remote"
+                             " files %s")
+            if repo.ui.promptchoice(promptmsg
+                    % (confilesstr, repo.wjoin(rfile), confilesstr),
+                    (_("&Abort"), _("&Delete")), 0):
+                # remove conflicting file additions from action
+                action = [a for a in action
+                          if a[1] != 'g' or util.normcase(a[0]).find(rdir) != 0]
+            else:
+                raise util.Abort(abortmsg % (repo.wjoin(rfile), confilesstr))
+
     return action
 
 def actionkey(a):
@@ -387,7 +453,26 @@
             flags = a[2]
             repo.ui.note(_("getting %s\n") % f)
             t = mctx.filectx(f).data()
-            repo.wwrite(f, t, flags)
+            try:
+                repo.wwrite(f, t, flags)
+            except OSError, err:
+                dirpath = repo.wjoin(f)
+                if (err.errno in (errno.EISDIR, errno.EPERM)
+                               and os.path.isdir(dirpath)
+                               and len(os.listdir(dirpath)) == 0):
+                    if repo.ui.promptchoice(
+                            _(" empty local dir %s conflicts"
+                            " with remote file %s\n(r)emove "
+                            "empty local directory or (a)bort? [r]")
+                            % (dirpath, f), (_("&Remove"), _("&Abort")), 0):
+                        raise util.Abort(
+                            _("cannot write file %s, conflicting empty "
+                              "directory %s") % (f, dirpath))
+                    else:
+                        os.rmdir(dirpath)
+                        repo.wwrite(f, t, flags)
+                else:
+                    raise
             t = None
             updated += 1
             if f == '.hgsubstate': # subrepo states need updating
diff -r d51364b318ea -r 395b8469087e tests/test-merge8.t
--- a/tests/test-merge8.t	Thu Jul 26 21:29:39 2012 +0200
+++ b/tests/test-merge8.t	Mon Oct 22 20:51:07 2012 -0700
@@ -1,5 +1,6 @@
-Test for changeset ba7c74081861
-(update dirstate correctly for non-branchmerge updates)
+Test for changeset ba7c74081861 (update dirstate correctly for non-branchmerge updates):
+and issue29 - http://mercurial.selenic.com/bts/issue29
+
   $ hg init a
   $ cd a
   $ echo a > a
@@ -26,4 +27,77 @@
   $ hg update
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
 
-  $ cd ..
+issue29 test
+  $ cd ../a
+  $ mkdir dir1
+  $ mkdir dir1/dir11
+  $ echo deepf > dir1/dir11/deepf
+  $ hg ci -Am m
+  adding dir1/dir11/deepf
+  $ cd ../b
+  $ hg pull -u ../a
+  pulling from ../a
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd ../a
+  $ mkdir emptydir
+  $ mkdir dirorfile
+  $ mkdir dirorfile/cdir
+  $ echo cfile > dirorfile/cdir/cfile
+  $ hg ci -Am m
+  adding dirorfile/cdir/cfile
+  $ cd ../b
+  $ touch dirorfile
+  $ hg ci -Am m
+  adding dirorfile
+  $ hg pull ../a
+  pulling from ../a
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+  $ hg merge
+   remote dirorfile/cdir/cfile conflicts with local $TESTTMP/b/dirorfile path
+  (a)bort or (d)elete remote file dirorfile/cdir/cfile? [a] a
+  abort: local file $TESTTMP/b/dirorfile conflicts with path of remote file dirorfile/cdir/cfile
+  [255]
+  $ cd ../a
+  $ hg pull ../b
+  pulling from ../b
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+  $ hg merge
+   remote dirorfile conflicts with local directory $TESTTMP/a/dirorfile
+  (a)bort operation or (d)elete remote file? [a] a
+  abort: remote dirorfile conflicts with local directory $TESTTMP/a/dirorfile
+  [255]
+  $ cd ../b
+  $ rm dirorfile
+  $ touch emptydir
+  $ hg ci -Am m
+  removing dirorfile
+  adding emptydir
+  $ cd ../a
+  $ hg pull ../b
+  pulling from ../b
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg merge
+   empty local dir $TESTTMP/a/emptydir conflicts with remote file emptydir
+  (r)emove empty local directory or (a)bort? [r] r
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)


More information about the Mercurial-devel mailing list