[PATCH 3 of 3] filemerge: add new internal merge tool 'internal:patch'

FUJIWARA Katsunori foozy at lares.dti.ne.jp
Sun Feb 12 06:40:00 CST 2012


# HG changeset patch
# User FUJIWARA Katsunori <foozy at lares.dti.ne.jp>
# Date 1329050292 -32400
# Node ID 4f2d7f20d502b7c98d43c85b9ae654fd65906bcf
# Parent  b2fad545c25b5f096a23db9aa6e29251d026726c
filemerge: add new internal merge tool 'internal:patch'

3-way merge places contents of 'local'/'base'/'other' side by side, so
sometime creates complained result which is difficult to resolve.

this patch adds new internal merge tool 'internal:patch', which
applies diff between 'base' and 'local' on 'other'.

this will create '*.rej' file storing rejected hunks, and it is easier
to recognize than 3-way merge result in some cases.


IMPLEMENTATION NOTES:

(A) this pach applies diff between 'base' and 'LOCAL' on 'OTHER', not
diff between 'base' and 'OTHER' on 'LOCAL'.

for merging on linear update, the former seems to be better, because
contents on 'OTHER' is already commited into history, but 'LOCAL' is
not yet.

and 'filemerge()' does not have information to distinguish it from
branch merging.


(B) this patch writes diff result into external file, and then applies
it by reading from file.

diff result returned by 'mdiff.unidiff()' is on memory, so StringIO
(or cStringIO) can be used for 'patch.internalpatch()' invocation.

but it may occupy much memory to hold whole diff result on memory, so
this patch choices to write diff result into external file briefly.

this expects that 'patch.internalpatch()' reads diff contents into
memory little by little, not at once.


(C) this patch may create dirstate entry for merge result file (call
it 'fd') temporarily because:

    - dirstate of 'fd' is not yet created on merging with branch on
      which file is renamed or copied

    - 'patch.internalpatch()' can not patch on untracked file

    - temporary renaming from 'fd' to original may need backup/restore
      for current original file and '*.rej' for it (, and
      implementation becomes complicated)

according to 'merge.manifestmerge()', 'filemerge.filemerge()' is
invoked in below 4 cases.

    1. both changed ('versions differ')
    2. case 2 A,B/B/B or case 4,21 A/B/B ('local copied/moved to')
    3. rename case 1, A/A,B/A ('remote copied to')
    4. case 3,20 A/B/A ('remote moved to')

in #1/#2 cases, 'fd' already exists on current context, so these can
be ignored.

both in #3/#4 cases, 'merge.recordupdates()' invokes below methods on
dirstate:

    - 'normallookup(fd)' for linear update merge
    - 'merge(fd)' and 'copy(f, fd)' for branch merge

according to current dirstate implementation, even if dirstate entry
for 'fd' is temporarily created, these can execute safely, if it is
dropped while 'filemerge()' execution.

this patch uses 'normallookup()' to create temporary dirstate entry,
because it does not cause any invocations of expensive call like
'os.lstat()'.

diff -r b2fad545c25b -r 4f2d7f20d502 mercurial/filemerge.py
--- a/mercurial/filemerge.py	Sun Feb 12 21:38:12 2012 +0900
+++ b/mercurial/filemerge.py	Sun Feb 12 21:38:12 2012 +0900
@@ -7,7 +7,7 @@
 
 from node import short
 from i18n import _
-import util, simplemerge, match, error
+import util, simplemerge, match, error, mdiff, patch
 import os, tempfile, re, filecmp
 
 def _toolstr(ui, tool, part, default=""):
@@ -232,6 +232,76 @@
         repo.wwrite(fd + ".base", fca.data(), fca.flags())
     return False, r
 
+ at internaltool('internal:patch', True,
+              _("merging %s incomplete! "
+                "(check rejected hunks, then use 'hg resolve --mark')\n"))
+def _ipatch(repo, mynode, orig, fcd, fco, fca, toolconf, files):
+    """``internal:patch``
+    Creates diff between base and local, then patches it on other.
+    This also creates a file for rejected hunks known as ``*.rej``.
+    You can resolve conflicts with it.
+    For this merging, other is prior to local, and rejected hunks come
+    from local contents, because other is already committed contents
+    but local is not for linear update merge."""
+
+    r = _premerge(repo, toolconf, files)
+    if r:
+        fd = fcd.path()
+        ui = repo.ui
+
+        fcactx = fca.changectx()
+        fcdctx = fcd.changectx()
+
+        prefix = "%s~patch." % (os.path.basename(fd))
+        (tmpfd, pfname) = tempfile.mkstemp(prefix=prefix)
+        ui.debug('writing diff result into %s\n' % (pfname))
+        f = os.fdopen(tmpfd, "wb")
+        f.write(mdiff.unidiff(fca.data(), util.datestr(fcactx.date()),
+                              fcd.data(), util.datestr(fcdctx.date()),
+                              fd, fd,
+                              [str(fcactx), str(fcdctx)]))
+        f.close()
+
+        if orig != fd:
+            if repo.dirstate[fd] == '?':
+                # merge with file renamed/copied on 'other' side
+
+                # the file to which 'orig' is renamed/copied on
+                # 'other' side has no dirstate entry at this point,
+                # because 'filemerge()' is invoked before dirstate
+                # updating for such files.
+
+                # 'patch.internalpatch()' prevents untracked file from
+                # being patched, so create dirstate for 'fd' temporarily.
+
+                # save copy marking, because 'normlookup()' discards it
+                copied = repo.dirstate.copied(fd)
+
+                # use 'normallookup()' because it is cheaper than 'merge()'
+                repo.dirstate.normallookup(fd)
+                dropdirstate = True
+            else:
+                dropdirstate = False
+
+        try:
+            repo.wwrite(fcd.path(), fco.data(), fco.flags())
+            try:
+                patch.internalpatch(ui, repo, pfname, strip=1, eolmode=None)
+                ui.warn(_("patching as merge unexpectedly succeeded, "
+                          "even though 3-way merge failed.\n"))
+            except patch.PatchError, err:
+                # EXPECTED FAILURE
+                ui.warn(str(err) + '\n')
+        finally:
+            if orig != fd:
+                if dropdirstate:
+                    repo.dirstate.drop(fd)
+                    repo.dirstate.copy(copied, fd)
+            if not ui.debugflag:
+                os.unlink(pfname)
+
+    return False, r
+
 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files):
     r = _premerge(repo, toolconf, files)
     if r:
diff -r b2fad545c25b -r 4f2d7f20d502 tests/test-merge-tools.t
--- a/tests/test-merge-tools.t	Sun Feb 12 21:38:12 2012 +0900
+++ b/tests/test-merge-tools.t	Sun Feb 12 21:38:12 2012 +0900
@@ -3,7 +3,8 @@
 test merge-tools configuration - mostly exercising filemerge.py
 
   $ unset HGMERGE # make sure HGMERGE doesn't interfere with the test
-  $ hg init
+  $ hg init repo1
+  $ cd repo1
 
 revision 0
 
@@ -770,3 +771,336 @@
   # hg stat
   M f
   ? f.orig
+
+  $ cd ..
+
+tests for internal:patch
+
+  $ hg init repo2
+  $ cd repo2
+
+  $ cat > .hg/hgrc <<EOF
+  > [ui]
+  > merge = internal:patch
+  > EOF
+
+  $ modify() {
+  >     mv $1 tmp
+  >     sed -f $2 tmp > $1
+  >     rm tmp
+  > }
+
+  $ cat > a.txt <<EOF
+  > 0
+  > 1
+  > 2
+  > 3
+  > 4
+  > 5
+  > 6
+  > 7
+  > 8
+  > 9
+  > A
+  > B
+  > C
+  > D
+  > E
+  > F
+  > EOF
+  $ hg add a.txt
+  $ hg commit -m '#0'
+
+  $ cat > ${HGTMP}/script <<EOF
+  > s/^0/0 modified at #1/g
+  > s/^8/8 modified at #1/g
+  > EOF
+  $ modify a.txt ${HGTMP}/script
+  $ hg commit -m '#1'
+
+  $ cat > ${HGTMP}/script <<EOF
+  > s/^7/7 modified at #2/g
+  > s/^F/F modified at #2/g
+  > EOF
+  $ modify a.txt ${HGTMP}/script
+  $ hg commit -m '#2'
+
+  $ hg update -C 1 > /dev/null
+  $ hg rename a.txt b.txt
+  $ hg commit -m '#3'
+  created new head
+
+  $ cat > ${HGTMP}/script <<EOF
+  > s/^7/7 modified at #4/g
+  > s/^F/F modified at #4/g
+  > EOF
+  $ modify b.txt ${HGTMP}/script
+  $ hg commit -m '#4'
+
+  $ hg update -C 0 > /dev/null
+  $ cat > ${HGTMP}/script <<EOF
+  > s/^7/7 modified at #5/g
+  > s/^F/F modified at #5/g
+  > EOF
+  $ modify a.txt ${HGTMP}/script
+  $ hg commit -m '#5'
+  created new head
+
+"linear update merge" for internal:patch
+
+  $ hg update -C 1 > /dev/null
+  $ cat > ${HGTMP}/script <<EOF
+  > s/^1/1 modified in working directory/g
+  > s/^F/F modified in working directory/g
+  > EOF
+  $ modify a.txt ${HGTMP}/script
+  $ hg update 2
+  merging a.txt
+  patching file a.txt
+  Hunk #2 FAILED at 12
+  1 out of 2 hunks FAILED -- saving rejects to file a.txt.rej
+  patch failed to apply
+  merging a.txt incomplete! (check rejected hunks, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges
+  [1]
+  $ cat a.txt
+  0 modified at #1
+  1 modified in working directory
+  2
+  3
+  4
+  5
+  6
+  7 modified at #2
+  8 modified at #1
+  9
+  A
+  B
+  C
+  D
+  E
+  F modified at #2
+  $ cat a.txt.rej
+  --- a.txt
+  +++ a.txt
+  @@ -13,4 +13,4 @@
+   C
+   D
+   E
+  -F
+  +F modified in working directory
+  $ hg status
+  M a.txt
+  ? a.txt.orig
+  ? a.txt.rej
+  $ hg resolve -l
+  U a.txt
+  $ rm -f a.txt.orig a.txt.rej
+
+"liner update merge: renaming on other side" for internal:patch
+
+  $ hg update -C 1 > /dev/null
+  $ cat > ${HGTMP}/script <<EOF
+  > s/^1/1 modified in working directory/g
+  > s/^F/F modified in working directory/g
+  > EOF
+  $ modify a.txt ${HGTMP}/script
+  $ hg update 4
+  merging a.txt and b.txt to b.txt
+  patching file b.txt
+  Hunk #2 FAILED at 12
+  1 out of 2 hunks FAILED -- saving rejects to file b.txt.rej
+  patch failed to apply
+  merging b.txt incomplete! (check rejected hunks, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges
+  [1]
+  $ cat b.txt
+  0 modified at #1
+  1 modified in working directory
+  2
+  3
+  4
+  5
+  6
+  7 modified at #4
+  8 modified at #1
+  9
+  A
+  B
+  C
+  D
+  E
+  F modified at #4
+  $ cat b.txt.rej
+  --- b.txt
+  +++ b.txt
+  @@ -13,4 +13,4 @@
+   C
+   D
+   E
+  -F
+  +F modified in working directory
+  $ hg status
+  M b.txt
+  ? b.txt.orig
+  ? b.txt.rej
+  $ hg resolve -l
+  U b.txt
+  $ rm -f b.txt.orig b.txt.rej
+
+"branch merge" for internal:patch
+
+  $ hg update -C 1 > /dev/null
+  $ hg merge 5
+  merging a.txt
+  patching file a.txt
+  Hunk #2 FAILED at 5
+  1 out of 2 hunks FAILED -- saving rejects to file a.txt.rej
+  patch failed to apply
+  merging a.txt incomplete! (check rejected hunks, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+  $ cat a.txt
+  0 modified at #1
+  1
+  2
+  3
+  4
+  5
+  6
+  7 modified at #5
+  8
+  9
+  A
+  B
+  C
+  D
+  E
+  F modified at #5
+  $ cat a.txt.rej
+  --- a.txt
+  +++ a.txt
+  @@ -6,7 +6,7 @@
+   5
+   6
+   7
+  -8
+  +8 modified at #1
+   9
+   A
+   B
+  $ hg status -A
+  M a.txt
+  ? a.txt.orig
+  ? a.txt.rej
+  $ hg resolve -l
+  U a.txt
+  $ rm -f a.txt.orig a.txt.rej
+
+"branch merge: renaming on local side" for internal:patch
+
+  $ hg update -C 3 > /dev/null
+  $ hg merge 5
+  merging b.txt and a.txt to b.txt
+  patching file b.txt
+  Hunk #2 FAILED at 5
+  1 out of 2 hunks FAILED -- saving rejects to file b.txt.rej
+  patch failed to apply
+  merging b.txt incomplete! (check rejected hunks, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+  $ cat b.txt
+  0 modified at #1
+  1
+  2
+  3
+  4
+  5
+  6
+  7 modified at #5
+  8
+  9
+  A
+  B
+  C
+  D
+  E
+  F modified at #5
+  $ cat b.txt.rej
+  --- b.txt
+  +++ b.txt
+  @@ -6,7 +6,7 @@
+   5
+   6
+   7
+  -8
+  +8 modified at #1
+   9
+   A
+   B
+  $ hg status -A
+  M b.txt
+    a.txt
+  ? b.txt.orig
+  ? b.txt.rej
+  $ hg resolve -l
+  U b.txt
+  $ rm -f b.txt.orig b.txt.rej
+
+"branch merge: renaming on other side" for internal:patch
+
+  $ hg update -C 5 > /dev/null
+  $ hg merge 3
+  merging a.txt and b.txt to b.txt
+  patching file b.txt
+  Hunk #1 FAILED at 4
+  1 out of 2 hunks FAILED -- saving rejects to file b.txt.rej
+  patch failed to apply
+  merging b.txt incomplete! (check rejected hunks, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+  $ cat b.txt
+  0 modified at #1
+  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8 modified at #1
+  9
+  A
+  B
+  C
+  D
+  E
+  F modified at #5
+  $ cat b.txt.rej
+  --- b.txt
+  +++ b.txt
+  @@ -5,7 +5,7 @@
+   4
+   5
+   6
+  -7
+  +7 modified at #5
+   8
+   9
+   A
+  $ hg status -A
+  M b.txt
+    a.txt
+  R a.txt
+  ? b.txt.orig
+  ? b.txt.rej
+  $ hg resolve -l
+  U b.txt
+  $ rm -f b.txt.orig b.txt.rej
+
+  $ cd ..


More information about the Mercurial-devel mailing list