diff --git a/changelog/57205.fixed.md b/changelog/57205.fixed.md new file mode 100644 index 000000000000..51391daacbeb --- /dev/null +++ b/changelog/57205.fixed.md @@ -0,0 +1 @@ +Fix `file.patch` state failing to detect an already-applied patch when the patch file contains absolute or prefixed paths and GNU patch 2.7.6+ is used. The reverse-apply dry-run now correctly passes `-p0` so the reject file is matched against the target without path stripping. diff --git a/salt/states/file.py b/salt/states/file.py index d16b6d021906..3bae62c5a9c4 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -7900,8 +7900,8 @@ def _patch(patch_file, options=None, dry_run=False): # Try to reverse-apply hunks from rejects file using a dry-run. # If this returns a retcode of 0, we know that the patch was # already applied. Rejects are written from the base of the - # directory, so the strip option doesn't apply here. - reverse_pass = _patch(patch_rejects, ["-R", "-f"], dry_run=True) + # directory, so the required strip level is 0. + reverse_pass = _patch(patch_rejects, ["-R", "-f", "-p0"], dry_run=True) already_applied = reverse_pass["retcode"] == 0 # Check if the patch command threw an error upon execution diff --git a/tests/pytests/functional/states/file/test_patch.py b/tests/pytests/functional/states/file/test_patch.py index a1c651f8980f..7797b3d0557c 100644 --- a/tests/pytests/functional/states/file/test_patch.py +++ b/tests/pytests/functional/states/file/test_patch.py @@ -188,6 +188,53 @@ def test_patch_single_file(file, files, patches): assert ret.comment == "Patch was already applied" +def test_patch_single_file_absolute_paths(file, state_tree, tmp_path): + """ + Regression test for issue #52329: file.patch must detect an already-applied + patch when the patch file uses absolute paths (GNU patch 2.7.6+ behaviour). + + Without the ``-p0`` flag on the reverse-apply dry-run, GNU patch cannot + match the absolute path in the reject file against the target file and + returns a non-zero exit code, causing the state to report + "Patch would not apply cleanly" instead of "Patch was already applied". + """ + target = tmp_path / "target.txt" + target.write_text("line one\nline two\nline three\n") + + # Construct a patch with the absolute path of the target file as the + # header (mirrors the real-world zeromq.patch from issue #52329). + abs_path = str(target) + patch_content = textwrap.dedent( + f"""\ + --- {abs_path} + +++ {abs_path} + @@ -1,3 +1,3 @@ + -line one + +LINE ONE + line two + line three + """ + ) + patch_file = tmp_path / "absolute.patch" + patch_file.write_text(patch_content) + + patch_source = "salt://absolute.patch" + patch_dest = state_tree / "absolute.patch" + patch_dest.write_text(patch_content) + + # First run: apply the patch. + ret = file.patch(name=abs_path, source=patch_source) + assert ret.result is True + assert ret.changes + assert ret.comment == "Patch successfully applied" + + # Second run: must detect the patch was already applied, not fail. + ret = file.patch(name=abs_path, source=patch_source) + assert ret.result is True + assert not ret.changes + assert ret.comment == "Patch was already applied" + + @pytest.mark.skip_on_freebsd( reason="Previously skipped on FreeBSD. Needs investigation as to why it currently False" )