merge: handle directory renames
authorMatt Mackall <mpm@selenic.com>
Thu, 30 Nov 2006 17:36:33 -0600
changeset 3733 9e67fecbfd16
parent 3732 ffe9fef84801
child 3734 216e75c721d3
merge: handle directory renames commit: handle new copy dirstate case correctly findcopies: keep a map of all copies found for directory logic add dirs filter check for merge:followdirs config option generate a directory move map find files that match directory move map manifestmerge: add directory rename cases applyupdates: skip actions with None file add "d" action recordupdates: add "d" action add simple directory rename test
mercurial/localrepo.py
mercurial/merge.py
tests/test-rename-dir-merge
--- a/mercurial/localrepo.py	Thu Nov 30 17:36:33 2006 -0600
+++ b/mercurial/localrepo.py	Thu Nov 30 17:36:33 2006 -0600
@@ -553,9 +553,11 @@
                 fp2 = nullid
             elif fp2 != nullid: # copied on remote side
                 meta["copyrev"] = hex(manifest1.get(cp, nullid))
-            else: # copied on local side, reversed
+            elif fp1 != nullid: # copied on local side, reversed
                 meta["copyrev"] = hex(manifest2.get(cp))
                 fp2 = nullid
+            else: # directory rename
+                meta["copyrev"] = hex(manifest1.get(cp, nullid))
             self.ui.debug(_(" %s: copy %s:%s\n") %
                           (fn, cp, meta["copyrev"]))
             fp1 = nullid
--- a/mercurial/merge.py	Thu Nov 30 17:36:33 2006 -0600
+++ b/mercurial/merge.py	Thu Nov 30 17:36:33 2006 -0600
@@ -122,11 +122,20 @@
                 return
             c2 = ctx(of, man[of])
             ca = c.ancestor(c2)
-            if not ca or c == ca or c2 == ca:
+            if not ca: # unrelated
                 return
             if ca.path() == c.path() or ca.path() == c2.path():
+                fullcopy[c.path()] = of
+                if c == ca or c2 == ca: # no merge needed, ignore copy
+                    return
                 copy[c.path()] = of
 
+    def dirs(files):
+        d = {}
+        for f in files:
+            d[os.path.dirname(f)] = True
+        return d
+
     if not repo.ui.configbool("merge", "followcopies", True):
         return {}
 
@@ -136,6 +145,7 @@
 
     dcopies = repo.dirstate.copies()
     copy = {}
+    fullcopy = {}
     u1 = nonoverlap(m1, m2, ma)
     u2 = nonoverlap(m2, m1, ma)
     ctx = util.cachefunc(lambda f, n: repo.filectx(f, fileid=n[:20]))
@@ -146,6 +156,38 @@
     for f in u2:
         checkcopies(ctx(f, m2[f]), m1)
 
+    if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
+        return copy
+
+    # generate a directory move map
+    d1, d2 = dirs(m1), dirs(m2)
+    invalid = {}
+    dirmove = {}
+
+    for dst, src in fullcopy.items():
+        dsrc, ddst = os.path.dirname(src), os.path.dirname(dst)
+        if dsrc in invalid:
+            continue
+        elif (dsrc in d1 and ddst in d1) or (dsrc in d2 and ddst in d2):
+            invalid[dsrc] = True
+        elif dsrc in dirmove and dirmove[dsrc] != ddst:
+            invalid[dsrc] = True
+            del dirmove[dsrc]
+        else:
+            dirmove[dsrc] = ddst
+
+    del d1, d2, invalid
+
+    if not dirmove:
+        return copy
+
+    # check unaccounted nonoverlapping files
+    for f in u1 + u2:
+        if f not in fullcopy:
+            d = os.path.dirname(f)
+            if d in dirmove:
+                copy[f] = dirmove[d] + "/" + os.path.basename(f)
+
     return copy
 
 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
@@ -210,7 +252,10 @@
             continue
         elif f in copy:
             f2 = copy[f]
-            if f2 in m1: # case 2 A,B/B/B
+            if f2 not in m2: # directory rename
+                act("remote renamed directory to " + f2, "d",
+                    f, None, f2, m1.execf(f))
+            elif f2 in m1: # case 2 A,B/B/B
                 act("local copied to " + f2, "m",
                     f, f2, f, fmerge(f, f2, f2), False)
             else: # case 4,21 A/B/B
@@ -238,7 +283,10 @@
             continue
         if f in copy:
             f2 = copy[f]
-            if f2 in m2: # rename case 1, A/A,B/A
+            if f2 not in m1: # directory rename
+                act("local renamed directory to " + f2, "d",
+                    None, f, f2, m2.execf(f))
+            elif f2 in m2: # rename case 1, A/A,B/A
                 act("remote copied to " + f, "m",
                     f2, f, f, fmerge(f2, f, f2), False)
             else: # case 3,20 A/B/A
@@ -264,7 +312,7 @@
     action.sort()
     for a in action:
         f, m = a[:2]
-        if f[0] == "/":
+        if f and f[0] == "/":
             continue
         if m == "r": # remove
             repo.ui.note(_("removing %s\n") % f)
@@ -300,6 +348,20 @@
             repo.wwrite(f, t)
             util.set_exec(repo.wjoin(f), flag)
             updated += 1
+        elif m == "d": # directory rename
+            f2, fd, flag = a[2:]
+            if f:
+                repo.ui.note(_("moving %s to %s\n") % (f, fd))
+                t = wctx.filectx(f).data()
+                repo.wwrite(fd, t)
+                util.set_exec(repo.wjoin(fd), flag)
+                util.unlink(repo.wjoin(f))
+            if f2:
+                repo.ui.note(_("getting %s to %s\n") % (f2, fd))
+                t = mctx.filectx(f2).data()
+                repo.wwrite(fd, t)
+                util.set_exec(repo.wjoin(fd), flag)
+            updated += 1
         elif m == "e": # exec
             flag = a[2]
             util.set_exec(repo.wjoin(f), flag)
@@ -345,6 +407,19 @@
                 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
                 if move:
                     repo.dirstate.forget([f])
+        elif m == "d": # directory rename
+            f2, fd, flag = a[2:]
+            if branchmerge:
+                repo.dirstate.update([fd], 'a')
+                if f:
+                    repo.dirstate.update([f], 'r')
+                    repo.dirstate.copy(f, fd)
+                if f2:
+                    repo.dirstate.copy(f2, fd)
+            else:
+                repo.dirstate.update([fd], 'n')
+                if f:
+                    repo.dirstate.forget([f])
 
 def update(repo, node, branchmerge, force, partial, wlock):
     """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-rename-dir-merge	Thu Nov 30 17:36:33 2006 -0600
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+mkdir t
+cd t
+hg init
+
+mkdir a
+echo foo > a/a
+echo bar > a/b
+
+hg add a
+hg ci -m "0" -d "0 0"
+
+hg co -C 0
+hg mv a b
+hg ci -m "1 mv a/ b/" -d "0 0"
+
+hg co -C 0
+echo baz > a/c
+hg add a/c
+hg ci -m "2 add a/c" -d "0 0"
+
+hg merge --debug 1
+ls a/ b/
+hg st -C
+hg ci -m "3 merge 2+1" -d "0 0"
+
+hg co -C 1
+hg merge --debug 2
+ls a/ b/
+hg st -C
+hg ci -m "4 merge 1+2" -d "0 0"