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)