OpenHands/openhands/runtime/utils/git_handler.py
Xingyao Wang c2f46200c0
chore(lint): Apply comprehensive linting and formatting fixes (#10287)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-13 21:13:19 +02:00

126 lines
4.5 KiB
Python

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
from openhands.core.logger import openhands_logger as logger
from openhands.runtime.utils import git_changes, git_diff
GIT_CHANGES_CMD = 'python3 /openhands/code/openhands/runtime/utils/git_changes.py'
GIT_DIFF_CMD = (
'python3 /openhands/code/openhands/runtime/utils/git_diff.py "{file_path}"'
)
@dataclass
class CommandResult:
"""Represents the result of a shell command execution.
Attributes:
content (str): The output content of the command.
exit_code (int): The exit code of the command execution.
"""
content: str
exit_code: int
class GitHandler:
"""A handler for executing Git-related operations via shell commands."""
def __init__(
self,
execute_shell_fn: Callable[[str, str | None], CommandResult],
create_file_fn: Callable[[str, str], int],
):
self.execute = execute_shell_fn
self.create_file_fn = create_file_fn
self.cwd: str | None = None
self.git_changes_cmd = GIT_CHANGES_CMD
self.git_diff_cmd = GIT_DIFF_CMD
def set_cwd(self, cwd: str) -> None:
"""Sets the current working directory for Git operations.
Args:
cwd (str): The directory path.
"""
self.cwd = cwd
def _create_python_script_file(self, file: str):
result = self.execute('mktemp -d', self.cwd)
script_file = Path(result.content.strip(), Path(file).name)
with open(file, 'r') as f:
self.create_file_fn(str(script_file), f.read())
result = self.execute(f'chmod +x "{script_file}"', self.cwd)
return script_file
def get_git_changes(self) -> list[dict[str, str]] | None:
"""Retrieves the list of changed files in Git repositories.
Examines each direct subdirectory of the workspace directory looking for git repositories
and returns the changes for each of these directories.
Optimized to use a single git command per repository for maximum performance.
Returns:
list[dict[str, str]] | None: A list of dictionaries containing file paths and statuses. None if no git repositories found.
"""
# If cwd is not set, return None
if not self.cwd:
return None
result = self.execute(self.git_changes_cmd, self.cwd)
if result.exit_code == 0:
try:
changes = json.loads(result.content)
return changes
except Exception:
logger.exception(
'GitHandler:get_git_changes:error',
extra={'content': result.content},
)
return None
if self.git_changes_cmd != GIT_CHANGES_CMD:
# We have already tried to add a script to the workspace - it did not work
return None
# We try to add a script for getting git changes to the runtime - legacy runtimes may be missing the script
logger.info(
'GitHandler:get_git_changes: adding git_changes script to runtime...'
)
script_file = self._create_python_script_file(git_changes.__file__)
self.git_changes_cmd = f'python3 {script_file}'
# Try again with the new changes cmd
return self.get_git_changes()
def get_git_diff(self, file_path: str) -> dict[str, str]:
"""Retrieves the original and modified content of a file in the repository.
Args:
file_path (str): Path to the file.
Returns:
dict[str, str]: A dictionary containing the original and modified content.
"""
# If cwd is not set, return None
if not self.cwd:
raise ValueError('no_dir_in_git_diff')
result = self.execute(self.git_diff_cmd.format(file_path=file_path), self.cwd)
if result.exit_code == 0:
diff = json.loads(result.content, strict=False)
return diff
if self.git_diff_cmd != GIT_DIFF_CMD:
# We have already tried to add a script to the workspace - it did not work
raise ValueError('error_in_git_diff')
# We try to add a script for getting git changes to the runtime - legacy runtimes may be missing the script
logger.info('GitHandler:get_git_diff: adding git_diff script to runtime...')
script_file = self._create_python_script_file(git_diff.__file__)
self.git_diff_cmd = f'python3 {script_file} "{{file_path}}"'
# Try again with the new changes cmd
return self.get_git_diff(file_path)