Repository: conan-io/conan

Issue:
tools.patch cant create files
To help us debug your issue please explain:

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.

Hello,

I am trying to apply some out-of-tree git patches, some of them create new files.
Some of them contain new files, and the tools.patch utility will then compail that absolute filepaths are not allowed (`/dev/null`).

Lacking this support, its cumbersome to add bugfixes and git commits in general.

Example patch (created with `git format-patch`):
```patch
From d0807313143bb35da65c2b858a2d9e17fd3fbf9e Mon Sep 17 00:00:00 2001
From: Norbert Lange <nolange79@gmail.com>
Date: Fri, 7 Jun 2019 21:49:19 +0200
Subject: [PATCH] add and remove file

---
 newfile | 1 +
 oldfile | 1 -
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 newfile
 delete mode 100644 oldfile

diff --git a/newfile b/newfile
new file mode 100644
index 0000000..fdedddf
--- /dev/null
+++ b/newfile
@@ -0,0 +1 @@
+Hello mean world
diff --git a/oldfile b/oldfile
deleted file mode 100644
index 32332e1..0000000
--- a/oldfile
+++ /dev/null
@@ -1 +0,0 @@
-Old litter
-- 
2.20.1
```

My environment is:
```
Debian Buster x64
Conan version 1.16.0
```



Discussion:
Hi @nolange !

Are you sure about the line feeds (LF vs CR/LF)? Usually this kind of error is related to patches created on Windows with CRLF and applied on Linux where is configured to LF. I had the same problem months ago. You can use _dos2unix_

Conan uses the python package [patch](https://pypi.org/project/patch) which is widely used.


Yeah, I am pretty sure about the linefeeds (No OS or tool around that would produce them). Its an [upstream issue](https://github.com/techtonik/python-patch/issues/28) then.

Still wont make it less useless to me =/
Hi @nolange !

I can confirm your scenario, I created a [gist](https://gist.github.com/uilianries/5532fcafcef88dad1ea4fd6e74b5670e) which reproduces your issues.

In fact is a limitation from upstream and that issue is from 2015, so I can't they will solve it.

@lasote We have few options here, it's an upstream issue, from patch. We could provide a patch, but sounds dirty, or we could move to another python package with the same proposal.
Hi all,

Conan has code dedicated to fill this functionality that the ``patch`` library is missing: 

https://github.com/conan-io/conan/blob/551ddac6425341a861258617a2fbd09faf4a71ba/conans/client/tools/files.py#L218

```python
for p in patchset:
        source = decode_clean(p.source, "a/")
        target = decode_clean(p.target, "b/")
        if "dev/null" in source:
            target = strip_path(target)
            hunks = [s.decode("utf-8") for s in p.hunks[0].text]
            new_file = "".join(hunk[1:] for hunk in hunks)
            save(target, new_file)
        elif "dev/null" in target:
            source = strip_path(source)
            os.unlink(source)
        else:
            items.append(p)
```

So, if it is not working properly, this could probably be fixed there, there might be some format or path that it is not taking into account. I think that would be easier and less risky than switching to a different patch package.

Still, some preliminary investigation of ``patch`` package alternatives would be useful, maybe for conan 2.0, @uilianries if you want to have a look.
@memsharded : I use `strip=1` (practically the standard for patch-files), and the code does "strip" before handling the added/removed files.
Might be that `/dev/null` has been stripped to `/null` before running into the code you quoted above?

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
@@ -208,7 +208,9 @@ def decode_clean(path, prefix):
         return path
 
     def strip_path(path):
-        tokens = path.split("/")[strip:]
+        tokens = path.split("/")
+        if len(tokens) > 1:
+            tokens = tokens[strip:]
         path = "/".join(tokens)
         if base_path:
             path = os.path.join(base_path, path)


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
@@ -105,6 +105,26 @@ def source(self):
         client.run("source .")
         self.assertFalse(os.path.exists(path))
 
+    def test_patch_strip_delete_no_folder(self):
+        conanfile = dedent("""
+            from conans import ConanFile, tools
+            class PatchConan(ConanFile):
+                def source(self):
+                    tools.patch(self.source_folder, "example.patch", strip=1)""")
+        patch = dedent("""
+            --- a/oldfile
+            +++ b/dev/null
+            @@ -0,1 +0,0 @@
+            -legacy code""")
+        client = TestClient()
+        client.save({"conanfile.py": conanfile,
+                     "example.patch": patch,
+                     "oldfile": "legacy code"})
+        path = os.path.join(client.current_folder, "oldfile")
+        self.assertTrue(os.path.exists(path))
+        client.run("source .")
+        self.assertFalse(os.path.exists(path))
+
     def test_patch_new_delete(self):
         conanfile = base_conanfile + '''
     def build(self):
@@ -133,6 +153,26 @@ def build(self):
                       client.out)
         self.assertIn("test/1.9.10@user/testing: OLD FILE=False", client.out)
 
+    def test_patch_new_strip(self):
+        conanfile = base_conanfile + '''
+    def build(self):
+        from conans.tools import load, save
+        patch_content = """--- /dev/null
++++ b/newfile
+@@ -0,0 +0,3 @@
++New file!
++New file!
++New file!
+"""
+        patch(patch_string=patch_content, strip=1)
+        self.output.info("NEW FILE=%s" % load("newfile"))
+'''
+        client = TestClient()
+        client.save({"conanfile.py": conanfile})
+        client.run("create . user/testing")
+        self.assertIn("test/1.9.10@user/testing: NEW FILE=New file!\nNew file!\nNew file!\n",
+                      client.out)
+
     def test_error_patch(self):
         file_content = base_conanfile + '''
     def build(self):