Repository: conan-io/conan

Issue:
[bug] conan.patch incorrectly applied
initial issue: https://github.com/conan-io/conan-center-index/issues/214
it turns out conan's `tools.patch` method applies patches incorrectly after the switch to `patch_ng` package. e.g. its behavior significantly differs from the `patch` command, or `git apply` / `git am`

test case to reproduce:
https://github.com/conan-io/conan/compare/develop...SSE4:patch_issue?expand=1#diff-11632e511bcb65a506566fb2157b051eR27
reduced (short) test case:
https://github.com/conan-io/conan/compare/develop...SSE4:patch_issue?expand=1#diff-11632e511bcb65a506566fb2157b051eR409

in terminal:
```
git clone git@github.com:boostorg/boost.git
cd boost
git checkout boost-1.70.0
curl https://raw.githubusercontent.com/conan-io/conan-center-index/5d3a76231eeab86d24dda08cb0ee92fa307ef50f/recipes/boost/all/patches/bcp_namespace_issues.patch > 1.patch
patch -p1 < 1.patch
cat Jamroot | grep "1\.70\.0"
cat Jamroot | grep "1\.71\.0"
```
there is no `1.71.0` in the output (but in case of conan, it is)

<!--
  Please don't forget to update the issue title.
  Include all applicable information to help us reproduce your problem.

  To help us debug your issue please explain:
-->

### Environment Details (include every applicable attribute)
  * Operating System+version: Windows 10 Enterprise
  * Compiler+version: N/A
  * Conan version: 1.19.2
  * Python version: 3.7.3

### Steps to reproduce (Include if Applicable)


### Logs (Executed commands with output) (Include/Attach if Applicable)

<!--
  Your log content should be related to the bug description, it can be:
  - Conan command output
  - Server output (Artifactory, conan_server)
-->



Discussion:
Thanks, @SSE4. It is not working the same.

Talking about the `boost` example, the one copy/pasted in your comment, which is the one in your test `ToolsFilesPatchTest::test_patch1`:

 * the first issue is that the patch file doesn't match the original file: it the original `Jamroot` file it says `constant BOOST_VERSION : 1.70.0 ;`, but the patch contains `constant BOOST_VERSION : 1.71.0 ;`:

```patch
From d0586e88122f41cd5ac9666db70c37d6f0fc7480 Mon Sep 17 00:00:00 2001
From: Peter Dimov <pdimov@gmail.com>
Date: Sun, 26 May 2019 18:49:12 +0300
Subject: [PATCH] Fix `bcp --namespace` issues

---
 Jamroot | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/Jamroot b/Jamroot
index e0d7c90f51d..4e913c2b3f3 100644
--- a/Jamroot
+++ b/Jamroot
@@ -140,7 +140,8 @@ import "class" : new ;
 import property-set ;
 import threadapi-feature ;  ## Comment
 import option ;
-import tools/boost_install/boost-install ;
+# Backslash because of `bcp --namespace`
+import tools/boost\_install/boost-install ;
 
 path-constant BOOST_ROOT : . ;
 constant BOOST_VERSION : 1.71.0 ;
@@ -311,8 +312,8 @@ rule boost-install ( libraries * )
 # stage and install targets via boost-install, above.
 rule boost-lib ( name : sources * : requirements * : default-build * : usage-requirements * )
 {
+    autolink = <link>shared:<define>BOOST_$(name:U)_DYN_LINK=1 ;
     name = boost_$(name) ;
-    autolink = <link>shared:<define>$(name:U)_DYN_LINK=1 ;
     lib $(name)
         : $(sources)
         : $(requirements) $(autolink)

```

 * Git is able to patch the file with `fuzz 1` (it means that one thing doesn't match):
   ```
   ⇒  patch -p1 < 1.patch
   patching file Jamroot
   Hunk #1 succeeded at 140 with fuzz 1.
   ```
 * Our `patch-ng` says nothing about:
   ```
   p1.patch: reading p1.patch
   p1.patch: total files: 1  total hunks: 2
   p1.patch: stripping 1 leading component(s) from:
   p1.patch:    b'a/Jamroot'
   p1.patch:    b'b/Jamroot'
   p1.patch: processing 1/1:	 b'Jamroot'
   p1.patch:  hunk no.1 for file b'Jamroot'  -- is ready to be patched
   p1.patch:  hunk no.2 for file b'Jamroot'  -- is ready to be patched
   p1.patch: processing target file b'Jamroot'
   p1.patch: hunk 1
   p1.patch: hunk 2
   p1.patch: successfully patched 1/1:	 b'Jamroot'
   ```
   but substitutes, not only the lines to patch, **but all the remaining lines in the block!!**. That's why it substitutes the line `constant BOOST_VERSION : 1.71.0 ;` too.  (FYI, if the mismatch is before the lines to substitute, it does fails with `WARNING: source file is different - b'Jamroot'`).

If we are able to make `patch-ng` to inform about the fuzzy matches, we can fail if there is any (I think it would be ok to fail if the patch cannot be applied perfectly), but if we cannot detect this situation... problably we should revert https://github.com/conan-io/conan/pull/5761
@SSE4 If the issue https://github.com/conan-io/conan-center-index/issues/214 was already there, then the problem is with the original `python-patch`, not with the `patch-ng` fork we are going to introduce for `1.20`, right? So, reverting #5761 wouldn't fix anything...
Indeed, the issue https://github.com/conan-io/conan-center-index/issues/214 has triggered with conan 1.19.2, which uses patch=1.16.
test for `patch_ng` module: https://github.com/conan-io/python-patch-ng/compare/master...conan-io:fuzzy_patch_issue?expand=1
Issue in `patch-ng`: https://github.com/conan-io/python-patch-ng/issues/5
Recipe that reproduces the error:

```python
from conans import ConanFile, tools
import os


class FuzzConan(ConanFile):
    name = "fuzz"
    version = "1.70.0"
    scm = {
        "type": "git",
        "subfolder": "boost",
        "url": "git@github.com:boostorg/boost.git",
        "revision": "boost-1.70.0"
     }

    def build(self):
        url = "https://raw.githubusercontent.com/conan-io/conan-center-index/5d3a76231eeab86d24dda08cb0ee92fa307ef50f/recipes/boost/all/patches/bcp_namespace_issues.patch"
        patch = "bcp_namespace_issues.patch"
        tools.download(url, patch)
        tools.patch(base_path="boost", patch_file=patch)
        content = tools.load(os.path.join("boost", "Jamroot"))
        assert "1.70.0" in content
        assert "1.71.0" not in content

```
Hi!

The project *python-patch-ng* doesn't offer fuzzy support and there is no comment/issue/pr about it. I found that when apply the patch, it uses the patch hunk as source and doesn't compare with the original source, that's it's overriding. I think I'll be able to release a fix today. I delay it because needed to understand first the rules for fuzzy patch, I found it on gnu patch project.

Also, I've thought to port GNU patch project, as it's complete in terms of features and is not a big project. I could do it as side project during the weekend, and maintaining it should not be a problem.

IMHO, unless it is extremely easy to fix and no other bugs may be introduced due to that fix (we are not providing a `patch` library, but something that works for the Conan purposes), I would detect the fuzzy patch and fail. Conan is about reproducibility, if the patch is not fuzzy and the sources change, then the patch should be updated too.

wdyt?
Totally agree! 
it's better if conan just refuses to apply patch rather applying it incorrectly
well, the PR https://github.com/conan-io/python-patch-ng/pull/6 I ready and now it accepts fuzzy patches.

However, it's the default behavior now. I Could add a new parameter to accept or not fuzz. Although, gnu patch accepts fuzzy by default, so accepting any fuzzy patch now sounds more natural according gnu patch.

We can't consider git apply (it doesn't allow fuzzy patch), because this tool is a generic patch tool, which includes svn and mercurial.

Resolving patch:
diff --git a/conans/client/tools/files.py b/conans/client/tools/files.py
--- a/conans/client/tools/files.py
+++ b/conans/client/tools/files.py
@@ -171,9 +171,16 @@ def check_sha256(file_path, signature):
     check_with_algorithm_sum("sha256", file_path, signature)
 
 
-def patch(base_path=None, patch_file=None, patch_string=None, strip=0, output=None):
-    """Applies a diff from file (patch_file)  or string (patch_string)
-    in base_path directory or current dir if None"""
+def patch(base_path=None, patch_file=None, patch_string=None, strip=0, output=None, fuzz=False):
+    """ Applies a diff from file (patch_file)  or string (patch_string)
+        in base_path directory or current dir if None
+    :param base_path: Base path where the patch should be applied.
+    :param patch_file: Patch file that should be applied.
+    :param patch_string: Patch string that should be applied.
+    :param strip: Number of folders to be stripped from the path.
+    :param output: Stream object.
+    :param fuzz: Should accept fuzzy patches.
+    """
 
     class PatchLogHandler(logging.Handler):
         def __init__(self):
@@ -203,7 +210,7 @@ def emit(self, record):
     if not patchset:
         raise ConanException("Failed to parse patch: %s" % (patch_file if patch_file else "string"))
 
-    if not patchset.apply(root=base_path, strip=strip):
+    if not patchset.apply(root=base_path, strip=strip, fuzz=fuzz):
         raise ConanException("Failed to apply patch: %s" % patch_file)
 
 


Test patch:
diff --git a/conans/test/unittests/tools/files_patch_test.py b/conans/test/unittests/tools/files_patch_test.py
--- a/conans/test/unittests/tools/files_patch_test.py
+++ b/conans/test/unittests/tools/files_patch_test.py
@@ -196,12 +196,12 @@ def test_add_new_file(self):
         conanfile = dedent("""
             from conans import ConanFile, tools
             import os
-            
+
             class ConanFileToolsTest(ConanFile):
                 name = "foobar"
                 version = "0.1.0"
                 exports_sources = "*"
-            
+
                 def build(self):
                     tools.patch(patch_file="add_files.patch")
                     assert os.path.isfile("foo.txt")
@@ -213,13 +213,13 @@ def build(self):
             From: Uilian Ries <uilianries@gmail.com>
             Date: Wed, 16 Oct 2019 14:31:34 -0300
             Subject: [PATCH] add foo
-            
+
             ---
              bar.txt | 3 ++-
              foo.txt | 3 +++
              2 files changed, 5 insertions(+), 1 deletion(-)
              create mode 100644 foo.txt
-            
+
             diff --git a/bar.txt b/bar.txt
             index 0f4ff3a..0bd3158 100644
             --- a/bar.txt
@@ -237,7 +237,7 @@ def build(self):
             +For us, there is no spring.
             +Just the wind that smells fresh before the storm.
             +
-            -- 
+            --
             2.23.0
 
 
@@ -278,3 +278,41 @@ def _build_and_check(self, tmp_dir, file_path, text_file, msg):
 
         content = load(text_file)
         self.assertEqual(content, msg)
+
+    def test_fuzzy_patch(self):
+        conanfile = dedent("""
+            from conans import ConanFile, tools
+            import os
+
+            class ConanFileToolsTest(ConanFile):
+                name = "fuzz"
+                version = "0.1.0"
+                exports_sources = "*"
+
+                def build(self):
+                    tools.patch(patch_file="fuzzy.patch", fuzz=True)
+        """)
+        source = dedent("""X
+Y
+Z""")
+        patch = dedent("""diff --git a/Jamroot b/Jamroot
+index a6981dd..0c08f09 100644
+--- a/Jamroot
++++ b/Jamroot
+@@ -1,3 +1,4 @@
+ X
+ YYYY
++V
+ W""")
+        expected = dedent("""X
+Y
+V
+Z""")
+        client = TestClient()
+        client.save({"conanfile.py": conanfile,
+                     "fuzzy.patch": patch,
+                     "Jamroot": source})
+        client.run("install .")
+        client.run("build .")
+        content = load(os.path.join(client.current_folder, "Jamroot"))
+        self.assertIn(expected, content)