Compare commits

...

1 Commits

Author SHA1 Message Date
openhands 65ed29623d Fix Changes pane to display file deletions correctly
Previously, the get_git_diff function would fail with an error when
trying to display diffs for deleted files because os.path.getsize()
raises FileNotFoundError for non-existent files.

This fix:
- Checks if the file exists before attempting to get its size
- Allows deleted files to be handled gracefully, returning their
  original content with empty modified content (as expected for diffs)
- Adds tests for committed, staged, and unstaged file deletions
- Updates the command injection test to reflect the new behavior
  (non-existent files no longer raise errors; they return empty content)

Fixes #13441
2026-03-17 03:28:54 +00:00
2 changed files with 43 additions and 7 deletions
+3 -1
View File
@@ -89,7 +89,9 @@ def _make_git_show_cmd(ref: str, repo_relative_path: str) -> str:
def get_git_diff(relative_file_path: str) -> dict[str, str]:
path = Path(os.getcwd(), relative_file_path).resolve()
if os.path.getsize(path) > MAX_FILE_SIZE_FOR_GIT_DIFF:
file_exists = path.exists()
# Only check file size if the file exists (deleted files won't exist)
if file_exists and os.path.getsize(path) > MAX_FILE_SIZE_FOR_GIT_DIFF:
raise ValueError('file_to_large')
closest_git_repo = get_closest_git_repo(path)
if not closest_git_repo:
+40 -6
View File
@@ -254,6 +254,33 @@ class TestGitHandler(unittest.TestCase):
}
assert diff == expected_diff
def test_get_git_diff_committed_delete(self):
"""Test that get_git_diff handles committed deleted files."""
diff = self.git_handler.get_git_diff('committed_delete.txt')
expected_diff = {
'original': 'committed_delete.txt\nLine 1\nLine 2\nLine 3',
'modified': '',
}
assert diff == expected_diff
def test_get_git_diff_staged_delete(self):
"""Test that get_git_diff handles staged deleted files."""
diff = self.git_handler.get_git_diff('staged_delete.txt')
expected_diff = {
'original': 'staged_delete.txt\nLine 1\nLine 2\nLine 3',
'modified': '',
}
assert diff == expected_diff
def test_get_git_diff_unstaged_delete(self):
"""Test that get_git_diff handles unstaged deleted files."""
diff = self.git_handler.get_git_diff('unstaged_delete.txt')
expected_diff = {
'original': 'unstaged_delete.txt\nLine 1\nLine 2\nLine 3',
'modified': '',
}
assert diff == expected_diff
def test_get_git_changes_fallback(self):
"""Test that get_git_changes falls back to creating a script file when needed."""
# Break the git changes command
@@ -300,9 +327,10 @@ class TestGitHandler(unittest.TestCase):
# A payload that would create the sentinel file if injection were possible
malicious_path = f'"; touch {sentinel}; echo "'
# get_git_diff should raise (no such file) rather than executing the payload
with self.assertRaises(ValueError):
self.git_handler.get_git_diff(malicious_path)
# get_git_diff should safely handle the malicious path without executing it
# For non-existent files (like malicious paths), it returns empty content
# The important check is that no shell injection occurs
self.git_handler.get_git_diff(malicious_path)
assert not os.path.exists(sentinel), (
'Shell injection succeeded: sentinel file was created'
@@ -349,9 +377,15 @@ class TestGitShowCmdBuilder:
def test_get_git_diff_file_too_large():
"""Raises ValueError('file_to_large') when the file exceeds the size limit."""
with patch('os.path.getsize', return_value=git_diff.MAX_FILE_SIZE_FOR_GIT_DIFF + 1):
with pytest.raises(ValueError, match='file_to_large'):
git_diff.get_git_diff('/nonexistent/path.txt')
with tempfile.TemporaryDirectory() as tmp_dir:
# Create an actual file so that path.exists() returns True
file_path = os.path.join(tmp_dir, 'large_file.txt')
Path(file_path).write_text('content')
with patch(
'os.path.getsize', return_value=git_diff.MAX_FILE_SIZE_FOR_GIT_DIFF + 1
):
with pytest.raises(ValueError, match='file_to_large'):
git_diff.get_git_diff(file_path)
def test_get_git_diff_no_repository():