Repository: qodo-ai/pr-agent

Issue:
load_large_diff generates incorrect patch if old file does not end in a newline
If part of a diff doesn't end in a newline (such as a file not ending in a newline) then the generated patch in [load_large_diff](https://github.com/Codium-ai/pr-agent/blob/bde594c9e854a6d6a2b4bde9abfa1002510f6f20/pr_agent/algo/utils.py#L582) might mash two lines together into one line.

Example:

```
patch = load_large_diff("test.py",
"""\
old content 1
some new content
another line
""",
"""
old content 1
old content 2""")

print(patch)
```

output (observe the line `-old content 2+some new content`):
```
--- 
+++ 
@@ -1,3 +1,3 @@
-
 old content 1
-old content 2+some new content
+another line
```

Discussion:
@mrT23: see https://github.com/Codium-ai/pr-agent/pull/1439

Solution patch:
diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py
index 9b49c060..f7a7d686 100644
--- a/pr_agent/algo/utils.py
+++ b/pr_agent/algo/utils.py
@@ -588,6 +588,8 @@ def load_large_diff(filename, new_file_content_str: str, original_file_content_s
         return ""
 
     try:
+        original_file_content_str = (original_file_content_str or "").rstrip() + "\n"
+        new_file_content_str = (new_file_content_str or "").rstrip() + "\n"
         diff = difflib.unified_diff(original_file_content_str.splitlines(keepends=True),
                                     new_file_content_str.splitlines(keepends=True))
         if get_settings().config.verbosity_level >= 2 and show_warning:


Test patch:
diff --git a/tests/unittest/test_bitbucket_provider.py b/tests/unittest/test_bitbucket_provider.py
index d883d55b..27c9cb0e 100644
--- a/tests/unittest/test_bitbucket_provider.py
+++ b/tests/unittest/test_bitbucket_provider.py
@@ -251,7 +251,7 @@ class TestBitbucketServerProvider:
             FilePatchInfo(
                 'file\nwith\nmultiple\nlines\nto\nemulate\na\nreal\nfile',
                 'readme\nwithout\nsome\nlines\nto\nsimulate\na\nreal\nfile',
-                '--- \n+++ \n@@ -1,9 +1,9 @@\n-file\n-with\n-multiple\n+readme\n+without\n+some\n lines\n to\n-emulate\n+simulate\n a\n real\n file',
+                '--- \n+++ \n@@ -1,9 +1,9 @@\n-file\n-with\n-multiple\n+readme\n+without\n+some\n lines\n to\n-emulate\n+simulate\n a\n real\n file\n',
                 'Readme.md',
                 edit_type=EDIT_TYPE.MODIFIED,
             )
@@ -273,7 +273,7 @@ class TestBitbucketServerProvider:
             FilePatchInfo(
                 'file\nwith\nsome\nlines\nto\nemulate\na\nreal\nfile',
                 'readme\nwithout\nsome\nlines\nto\nsimulate\na\nreal\nfile',
-                '--- \n+++ \n@@ -1,9 +1,9 @@\n-file\n-with\n+readme\n+without\n some\n lines\n to\n-emulate\n+simulate\n a\n real\n file',
+                '--- \n+++ \n@@ -1,9 +1,9 @@\n-file\n-with\n+readme\n+without\n some\n lines\n to\n-emulate\n+simulate\n a\n real\n file\n',
                 'Readme.md',
                 edit_type=EDIT_TYPE.MODIFIED,
             )
@@ -295,7 +295,7 @@ class TestBitbucketServerProvider:
             FilePatchInfo(
                 'file\nwith\nsome\nlines\nto\nemulate\na\nreal\nfile',
                 'readme\nwithout\nsome\nlines\nto\nsimulate\na\nreal\nfile',
-                '--- \n+++ \n@@ -1,9 +1,9 @@\n-file\n-with\n+readme\n+without\n some\n lines\n to\n-emulate\n+simulate\n a\n real\n file',
+                '--- \n+++ \n@@ -1,9 +1,9 @@\n-file\n-with\n+readme\n+without\n some\n lines\n to\n-emulate\n+simulate\n a\n real\n file\n',
                 'Readme.md',
                 edit_type=EDIT_TYPE.MODIFIED,
             )
diff --git a/tests/unittest/test_extend_patch.py b/tests/unittest/test_extend_patch.py
index dfe4c502..71537d48 100644
--- a/tests/unittest/test_extend_patch.py
+++ b/tests/unittest/test_extend_patch.py
@@ -3,6 +3,7 @@ import pytest
 from pr_agent.algo.git_patch_processing import extend_patch
 from pr_agent.algo.pr_processing import pr_generate_extended_diff
 from pr_agent.algo.token_handler import TokenHandler
+from pr_agent.algo.utils import load_large_diff
 from pr_agent.config_loader import get_settings
 
 
@@ -157,3 +158,34 @@ class TestExtendedPatchMoreLines:
 
         p0_extended = patches_extended_with_extra_lines[0].strip()
         assert p0_extended == "## File: 'file1'\n\n@@ -3,8 +3,8 @@ \n line0\n line1\n-original content\n+modified content\n line2\n line3\n line4\n line5\n line6"
+
+
+class TestLoadLargeDiff:
+    def test_no_newline(self):
+        patch = load_large_diff("test.py",
+                                """\
+                                old content 1
+                                some new content
+                                another line
+                                """,
+                                """
+                                old content 1
+                                old content 2""")
+
+        patch_expected="""\
+--- 
++++ 
+@@ -1,3 +1,3 @@
+-
+                                 old content 1
+-                                old content 2
++                                some new content
++                                another line
+"""
+        assert patch == patch_expected
+
+    def test_empty_inputs(self):
+        assert load_large_diff("test.py", "", "") == ""
+        assert load_large_diff("test.py", None, None) == ""
+        assert (load_large_diff("test.py", "content\n", "") ==
+                '--- \n+++ \n@@ -1 +1 @@\n-\n+content\n')
\ No newline at end of file