[PATCH STABLE] icasefs: improve rename awareness of case-folding collision detection (issue3452)

FUJIWARA Katsunori foozy at lares.dti.ne.jp
Thu Jan 31 19:44:29 CST 2013


# HG changeset patch
# User FUJIWARA Katsunori <foozy at lares.dti.ne.jp>
# Date 1359682698 -32400
# Branch stable
# Node ID 3eca4bf6f77f64a83835466f9b07022f5152a219
# Parent  2a1fac3650a5b4d650198604c82ab59969500374
icasefs: improve rename awareness of case-folding collision detection (issue3452)

Before this patch, merging at (6) in the example below is aborted for
case-folding collision unexpectedly, because 'a' at (4) and 'A' at (5)
aren't recognized as source/destination of renaming.

  (0) -- (2) -- (4) -------
      \      \             \
       \      \             \
         (1) -- (3) -- (5) -- (6)

  0: add file 'a'
  1: rename from 'a' to 'A'
  2: add file 'x'
  3: merge
  4: modify 'a'
  5: modify 'A'
  6: merge

"copies.pathcopies()" used for case-folding collision detection scans
filelog entries only for the revisions which are grater than common
ancestor of merged revisions: in the example above, renaming from 'a'
to 'A' at (1) is not scanned in the merging at (6), because common
ancestor of merged revisions is (2) in this case.

"copies.mergecopies()" can detect renaming in such case.

But in the other hand, in the case merging branches without
modification of renamed file (= issue3370 case), like (3) in the
example above, "copies.mergecopies()" can't detect renaming.

So, this patch uses both "copies.pathcopies()" and
"copies.mergecopies()".

To prevent "copies.mergecopies()" from being called in
"manifestmerge()" again, "_checkcollision()" returns result of it, and
"manifestmerge()" reuses it, if it is invoked in "_checkcollision()".

diff --git a/hgext/largefiles/overrides.py b/hgext/largefiles/overrides.py
--- a/hgext/largefiles/overrides.py
+++ b/hgext/largefiles/overrides.py
@@ -360,8 +360,9 @@
 # Finally, the merge.applyupdates function will then take care of
 # writing the files into the working copy and lfcommands.updatelfiles
 # will update the largefiles.
-def overridemanifestmerge(origfn, repo, p1, p2, pa, overwrite, partial):
-    actions = origfn(repo, p1, p2, pa, overwrite, partial)
+def overridemanifestmerge(origfn, repo, p1, p2, pa, overwrite, partial,
+                          mergecopies=None):
+    actions = origfn(repo, p1, p2, pa, overwrite, partial, mergecopies)
     processed = []
 
     for action in actions:
diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -141,21 +141,33 @@
     if extractxs:
         wctx, actx = extractxs
         # class to delay looking up copy mapping
-        class pathcopies(object):
+        class copymap(object):
+            def __init__(self):
+                self._mergecopies = None
             @util.propertycache
-            def map(self):
+            def pathcopies(self):
                 # {dst at mctx: src at wctx} copy mapping
                 return copies.pathcopies(wctx, mctx)
-        pc = pathcopies()
+            @util.propertycache
+            def mergecopies(self):
+                self._mergecopies = copies.mergecopies(wctx._repo,
+                                                       wctx, mctx, actx)
+                return self._mergecopies[0]
+            def find(self, f1, f2):
+                return (self.pathcopies.get(f1) == f2 or
+                        self.mergecopies.get(f1) == f2 or
+                        self.mergecopies.get(f2) == f1)
+        cm = copymap()
 
         for fn in wctx:
             fold = util.normcase(fn)
             mfn = folded.get(fold, None)
-            if (mfn and mfn != fn and pc.map.get(mfn) != fn and
+            if (mfn and mfn != fn and not cm.find(mfn, fn) and
                 _remains(fn, wctx.manifest(), actx.manifest(), True) and
                 _remains(mfn, mctx.manifest(), actx.manifest())):
                 raise util.Abort(_("case-folding collision between %s and %s")
                                  % (mfn, fn))
+        return cm._mergecopies
 
 def _forgetremoved(wctx, mctx, branchmerge):
     """
@@ -185,7 +197,7 @@
 
     return actions
 
-def manifestmerge(repo, p1, p2, pa, overwrite, partial):
+def manifestmerge(repo, p1, p2, pa, overwrite, partial, mergecopies=None):
     """
     Merge p1 and p2 with ancestor pa and generate merge action list
 
@@ -204,7 +216,7 @@
     elif pa == p2: # backwards
         pa = p1.p1()
     elif pa and repo.ui.configbool("merge", "followcopies", True):
-        ret = copies.mergecopies(repo, p1, p2, pa)
+        ret = mergecopies or copies.mergecopies(repo, p1, p2, pa)
         copy, movewithdir, diverge, renamedelete = ret
         for of, fl in diverge.iteritems():
             act("divergent renames", "dr", of, fl)
@@ -440,13 +452,14 @@
     "Calculate the actions needed to merge mctx into tctx"
     actions = []
     folding = not util.checkcase(repo.path)
+    mergecopies = None
     if folding:
         # collision check is not needed for clean update
         if (not branchmerge and
             (force or not tctx.dirty(missing=True, branch=False))):
             _checkcollision(mctx, None)
         else:
-            _checkcollision(mctx, (tctx, ancestor))
+            mergecopies = _checkcollision(mctx, (tctx, ancestor))
     if not force:
         _checkunknown(repo, tctx, mctx)
     if tctx.rev() is None:
@@ -454,7 +467,8 @@
     actions += manifestmerge(repo, tctx, mctx,
                              ancestor,
                              force and not branchmerge,
-                             partial)
+                             partial,
+                             mergecopies)
     return actions
 
 def recordupdates(repo, actions, branchmerge):
diff --git a/tests/test-casecollision-merge.t b/tests/test-casecollision-merge.t
--- a/tests/test-casecollision-merge.t
+++ b/tests/test-casecollision-merge.t
@@ -22,33 +22,53 @@
   $ hg commit -m '#1'
   $ hg update 0
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
-  $ echo 'modified at #2' > a
+  $ echo x > x
+  $ hg add x
   $ hg commit -m '#2'
   created new head
 
-  $ hg merge
-  merging a and A to A
-  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
+  $ hg merge -q
+  $ hg status -A
+  M A
+  R a
+  C x
+
+  $ hg update -q --clean 1
+  $ hg merge -q
+  $ hg status -A
+  M x
+  C A
+
+additional test for issue3452:
+
+  $ hg commit -m '#3'
+
+  $ hg update -q --clean 2
+  $ echo 'a at 4' > a
+  $ hg commit -m '#4'
+  created new head
+
+  $ hg update -q --clean 3
+  $ echo 'A at 5' > A
+  $ hg commit -m '#5'
+
+  $ hg merge -q --tool internal:other 4
   $ hg status -A
   M A
     a
-  R a
+  C x
   $ cat A
-  modified at #2
+  a at 4
 
-  $ hg update --clean 1
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg merge
-  merging A and a to A
-  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
-  $ hg status -A
+  $ hg update -q --clean 4
+  $ hg merge -q --tool internal:other 5
+  $ hg statu -A
   M A
     a
+  R a
+  C x
   $ cat A
-  modified at #2
-
+  A at 5
   $ cd ..
 
 (2) colliding file is not related to collided file


More information about the Mercurial-devel mailing list