mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Add support for pre-commit.sh git hook (#8095)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
e5aad11995
commit
64b5c0a604
73
.openhands/pre-commit.sh
Normal file
73
.openhands/pre-commit.sh
Normal file
@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Running OpenHands pre-commit hook..."
|
||||
|
||||
# Store the exit code to return at the end
|
||||
# This allows us to be additive to existing pre-commit hooks
|
||||
EXIT_CODE=0
|
||||
|
||||
# Check if frontend directory has changed
|
||||
frontend_changes=$(git diff --cached --name-only | grep "^frontend/")
|
||||
if [ -n "$frontend_changes" ]; then
|
||||
echo "Frontend changes detected. Running frontend checks..."
|
||||
|
||||
# Check if frontend directory exists
|
||||
if [ -d "frontend" ]; then
|
||||
# Change to frontend directory
|
||||
cd frontend || exit 1
|
||||
|
||||
# Run lint:fix
|
||||
echo "Running npm lint:fix..."
|
||||
npm run lint:fix
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Frontend linting failed. Please fix the issues before committing."
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
# Run build
|
||||
echo "Running npm build..."
|
||||
npm run build
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Frontend build failed. Please fix the issues before committing."
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
# Run tests
|
||||
echo "Running npm test..."
|
||||
npm test
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Frontend tests failed. Please fix the failing tests before committing."
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
# Return to the original directory
|
||||
cd ..
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "Frontend checks passed!"
|
||||
fi
|
||||
else
|
||||
echo "Frontend directory not found. Skipping frontend checks."
|
||||
fi
|
||||
else
|
||||
echo "No frontend changes detected. Skipping frontend checks."
|
||||
fi
|
||||
|
||||
# Run any existing pre-commit hooks that might have been installed by the user
|
||||
# This makes our hook additive rather than replacing existing hooks
|
||||
if [ -f ".git/hooks/pre-commit.local" ]; then
|
||||
echo "Running existing pre-commit hooks..."
|
||||
bash .git/hooks/pre-commit.local
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Existing pre-commit hooks failed."
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "All pre-commit checks passed!"
|
||||
else
|
||||
echo "Some pre-commit checks failed. Please fix the issues before committing."
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
@ -2,4 +2,11 @@
|
||||
|
||||
echo "Setting up the environment..."
|
||||
|
||||
# Install pre-commit package
|
||||
python -m pip install pre-commit
|
||||
|
||||
# Install pre-commit hooks if .git directory exists
|
||||
if [ -d ".git" ]; then
|
||||
echo "Installing pre-commit hooks..."
|
||||
pre-commit install
|
||||
fi
|
||||
|
||||
@ -99,6 +99,13 @@ check out our [documentation](https://docs.all-hands.dev/modules/usage/getting-s
|
||||
There you'll find resources on how to use different LLM providers,
|
||||
troubleshooting resources, and advanced configuration options.
|
||||
|
||||
### Custom Scripts
|
||||
|
||||
OpenHands supports custom scripts that run at different points in the runtime lifecycle:
|
||||
|
||||
- **setup.sh**: Place this script in the `.openhands` directory of your repository to run custom setup commands when the runtime initializes.
|
||||
- **pre-commit.sh**: Place this script in the `.openhands` directory to add a custom git pre-commit hook that runs before each commit. This can be used to enforce code quality standards, run tests, or perform other checks before allowing commits.
|
||||
|
||||
## 🤝 How to Join the Community
|
||||
|
||||
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
|
||||
|
||||
@ -310,6 +310,7 @@ export enum I18nKey {
|
||||
STATUS$PREPARING_CONTAINER = "STATUS$PREPARING_CONTAINER",
|
||||
STATUS$CONTAINER_STARTED = "STATUS$CONTAINER_STARTED",
|
||||
STATUS$SETTING_UP_WORKSPACE = "STATUS$SETTING_UP_WORKSPACE",
|
||||
STATUS$SETTING_UP_GIT_HOOKS = "STATUS$SETTING_UP_GIT_HOOKS",
|
||||
ACCOUNT_SETTINGS_MODAL$DISCONNECT = "ACCOUNT_SETTINGS_MODAL$DISCONNECT",
|
||||
ACCOUNT_SETTINGS_MODAL$SAVE = "ACCOUNT_SETTINGS_MODAL$SAVE",
|
||||
ACCOUNT_SETTINGS_MODAL$CLOSE = "ACCOUNT_SETTINGS_MODAL$CLOSE",
|
||||
|
||||
@ -4388,6 +4388,21 @@
|
||||
"tr": "Çalışma alanı ayarlanıyor...",
|
||||
"ja": "ワークスペースを設定中..."
|
||||
},
|
||||
"STATUS$SETTING_UP_GIT_HOOKS": {
|
||||
"en": "Setting up git hooks...",
|
||||
"zh-CN": "正在设置 git 钩子...",
|
||||
"zh-TW": "正在設置 git 鉤子...",
|
||||
"de": "Git-Hooks werden eingerichtet...",
|
||||
"ko-KR": "git 훅을 설정하는 중...",
|
||||
"no": "Setter opp git-hooks...",
|
||||
"it": "Configurazione degli hook git...",
|
||||
"pt": "Configurando hooks do git...",
|
||||
"es": "Configurando hooks de git...",
|
||||
"ar": "جاري إعداد خطافات git...",
|
||||
"fr": "Configuration des hooks git...",
|
||||
"tr": "Git kancaları ayarlanıyor...",
|
||||
"ja": "git フックを設定中..."
|
||||
},
|
||||
"ACCOUNT_SETTINGS_MODAL$DISCONNECT": {
|
||||
"en": "Disconnect",
|
||||
"es": "Desconectar",
|
||||
|
||||
@ -126,6 +126,8 @@ def initialize_repository_for_runtime(
|
||||
)
|
||||
# Run setup script if it exists
|
||||
runtime.maybe_run_setup_script()
|
||||
# Set up git hooks if pre-commit.sh exists
|
||||
runtime.maybe_setup_git_hooks()
|
||||
|
||||
return repo_directory
|
||||
|
||||
|
||||
@ -131,7 +131,7 @@ class GitLabService(BaseGitService, GitService):
|
||||
|
||||
payload = {
|
||||
'query': query,
|
||||
'variables': variables,
|
||||
'variables': variables if variables is not None else {},
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
|
||||
@ -72,6 +72,7 @@ STATUS_MESSAGES = {
|
||||
'STATUS$CONTAINER_STARTED': 'Container started.',
|
||||
'STATUS$WAITING_FOR_CLIENT': 'Waiting for client...',
|
||||
'STATUS$SETTING_UP_WORKSPACE': 'Setting up workspace...',
|
||||
'STATUS$SETTING_UP_GIT_HOOKS': 'Setting up git hooks...',
|
||||
}
|
||||
|
||||
|
||||
@ -424,6 +425,97 @@ class Runtime(FileEditRuntimeMixin):
|
||||
if isinstance(obs, CmdOutputObservation) and obs.exit_code != 0:
|
||||
self.log('error', f'Setup script failed: {obs.content}')
|
||||
|
||||
def maybe_setup_git_hooks(self):
|
||||
"""Set up git hooks if .openhands/pre-commit.sh exists in the workspace or repository."""
|
||||
pre_commit_script = '.openhands/pre-commit.sh'
|
||||
read_obs = self.read(FileReadAction(path=pre_commit_script))
|
||||
if isinstance(read_obs, ErrorObservation):
|
||||
return
|
||||
|
||||
if self.status_callback:
|
||||
self.status_callback(
|
||||
'info', 'STATUS$SETTING_UP_GIT_HOOKS', 'Setting up git hooks...'
|
||||
)
|
||||
|
||||
# Ensure the git hooks directory exists
|
||||
action = CmdRunAction('mkdir -p .git/hooks')
|
||||
obs = self.run_action(action)
|
||||
if isinstance(obs, CmdOutputObservation) and obs.exit_code != 0:
|
||||
self.log('error', f'Failed to create git hooks directory: {obs.content}')
|
||||
return
|
||||
|
||||
# Make the pre-commit script executable
|
||||
action = CmdRunAction(f'chmod +x {pre_commit_script}')
|
||||
obs = self.run_action(action)
|
||||
if isinstance(obs, CmdOutputObservation) and obs.exit_code != 0:
|
||||
self.log(
|
||||
'error', f'Failed to make pre-commit script executable: {obs.content}'
|
||||
)
|
||||
return
|
||||
|
||||
# Check if there's an existing pre-commit hook
|
||||
pre_commit_hook = '.git/hooks/pre-commit'
|
||||
pre_commit_local = '.git/hooks/pre-commit.local'
|
||||
|
||||
# Read the existing pre-commit hook if it exists
|
||||
read_obs = self.read(FileReadAction(path=pre_commit_hook))
|
||||
if not isinstance(read_obs, ErrorObservation):
|
||||
# If the existing hook wasn't created by OpenHands, preserve it
|
||||
if 'This hook was installed by OpenHands' not in read_obs.content:
|
||||
self.log('info', 'Preserving existing pre-commit hook')
|
||||
# Move the existing hook to pre-commit.local
|
||||
action = CmdRunAction(f'mv {pre_commit_hook} {pre_commit_local}')
|
||||
obs = self.run_action(action)
|
||||
if isinstance(obs, CmdOutputObservation) and obs.exit_code != 0:
|
||||
self.log(
|
||||
'error',
|
||||
f'Failed to preserve existing pre-commit hook: {obs.content}',
|
||||
)
|
||||
return
|
||||
|
||||
# Make it executable
|
||||
action = CmdRunAction(f'chmod +x {pre_commit_local}')
|
||||
obs = self.run_action(action)
|
||||
if isinstance(obs, CmdOutputObservation) and obs.exit_code != 0:
|
||||
self.log(
|
||||
'error',
|
||||
f'Failed to make preserved hook executable: {obs.content}',
|
||||
)
|
||||
return
|
||||
|
||||
# Create the pre-commit hook that calls our script
|
||||
pre_commit_hook_content = f"""#!/bin/bash
|
||||
# This hook was installed by OpenHands
|
||||
# It calls the pre-commit script in the .openhands directory
|
||||
|
||||
if [ -x "{pre_commit_script}" ]; then
|
||||
source "{pre_commit_script}"
|
||||
exit $?
|
||||
else
|
||||
echo "Warning: {pre_commit_script} not found or not executable"
|
||||
exit 0
|
||||
fi
|
||||
"""
|
||||
|
||||
# Write the pre-commit hook
|
||||
write_obs = self.write(
|
||||
FileWriteAction(path=pre_commit_hook, content=pre_commit_hook_content)
|
||||
)
|
||||
if isinstance(write_obs, ErrorObservation):
|
||||
self.log('error', f'Failed to write pre-commit hook: {write_obs.content}')
|
||||
return
|
||||
|
||||
# Make the pre-commit hook executable
|
||||
action = CmdRunAction(f'chmod +x {pre_commit_hook}')
|
||||
obs = self.run_action(action)
|
||||
if isinstance(obs, CmdOutputObservation) and obs.exit_code != 0:
|
||||
self.log(
|
||||
'error', f'Failed to make pre-commit hook executable: {obs.content}'
|
||||
)
|
||||
return
|
||||
|
||||
self.log('info', 'Git pre-commit hook installed successfully')
|
||||
|
||||
def get_microagents_from_selected_repo(
|
||||
self, selected_repository: str | None
|
||||
) -> list[BaseMicroagent]:
|
||||
|
||||
@ -333,6 +333,7 @@ class AgentSession:
|
||||
git_provider_tokens, selected_repository, selected_branch
|
||||
)
|
||||
await call_sync_from_async(self.runtime.maybe_run_setup_script)
|
||||
await call_sync_from_async(self.runtime.maybe_setup_git_hooks)
|
||||
|
||||
self.logger.debug(
|
||||
f'Runtime initialized with plugins: {[plugin.name for plugin in self.runtime.plugins]}'
|
||||
|
||||
158
tests/unit/test_git_hooks.py
Normal file
158
tests/unit/test_git_hooks.py
Normal file
@ -0,0 +1,158 @@
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.events.action import CmdRunAction, FileReadAction
|
||||
from openhands.events.observation import (
|
||||
CmdOutputObservation,
|
||||
ErrorObservation,
|
||||
FileReadObservation,
|
||||
)
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
class TestGitHooks:
|
||||
@pytest.fixture
|
||||
def mock_runtime(self):
|
||||
# Create a mock runtime
|
||||
mock_runtime = MagicMock(spec=Runtime)
|
||||
mock_runtime.status_callback = None
|
||||
|
||||
# Set up read to return different values based on the path
|
||||
def mock_read(action):
|
||||
if action.path == '.openhands/pre-commit.sh':
|
||||
return FileReadObservation(
|
||||
content="#!/bin/bash\necho 'Test pre-commit hook'\nexit 0",
|
||||
path='.openhands/pre-commit.sh',
|
||||
)
|
||||
elif action.path == '.git/hooks/pre-commit':
|
||||
# Simulate no existing pre-commit hook
|
||||
return ErrorObservation(content='File not found')
|
||||
return ErrorObservation(content='Unexpected path')
|
||||
|
||||
mock_runtime.read.side_effect = mock_read
|
||||
|
||||
mock_runtime.run_action.return_value = CmdOutputObservation(
|
||||
content='', exit_code=0, command='test command'
|
||||
)
|
||||
mock_runtime.write.return_value = None
|
||||
return mock_runtime
|
||||
|
||||
def test_maybe_setup_git_hooks_success(self, mock_runtime):
|
||||
# Test successful setup of git hooks
|
||||
Runtime.maybe_setup_git_hooks(mock_runtime)
|
||||
|
||||
# Verify that the runtime tried to read the pre-commit script
|
||||
assert mock_runtime.read.call_args_list[0] == call(
|
||||
FileReadAction(path='.openhands/pre-commit.sh')
|
||||
)
|
||||
|
||||
# Verify that the runtime created the git hooks directory
|
||||
# We can't directly compare the CmdRunAction objects, so we check if run_action was called
|
||||
assert mock_runtime.run_action.called
|
||||
|
||||
# Verify that the runtime made the pre-commit script executable
|
||||
# We can't directly compare the CmdRunAction objects, so we check if run_action was called
|
||||
assert mock_runtime.run_action.called
|
||||
|
||||
# Verify that the runtime wrote the pre-commit hook
|
||||
assert mock_runtime.write.called
|
||||
|
||||
# Verify that the runtime made the pre-commit hook executable
|
||||
# We can't directly compare the CmdRunAction objects, so we check if run_action was called
|
||||
assert mock_runtime.run_action.call_count >= 3
|
||||
|
||||
# Verify that the runtime logged success
|
||||
mock_runtime.log.assert_called_with(
|
||||
'info', 'Git pre-commit hook installed successfully'
|
||||
)
|
||||
|
||||
def test_maybe_setup_git_hooks_no_script(self, mock_runtime):
|
||||
# Test when pre-commit script doesn't exist
|
||||
mock_runtime.read.side_effect = lambda action: ErrorObservation(
|
||||
content='File not found'
|
||||
)
|
||||
|
||||
Runtime.maybe_setup_git_hooks(mock_runtime)
|
||||
|
||||
# Verify that the runtime tried to read the pre-commit script
|
||||
mock_runtime.read.assert_called_with(
|
||||
FileReadAction(path='.openhands/pre-commit.sh')
|
||||
)
|
||||
|
||||
# Verify that no other actions were taken
|
||||
mock_runtime.run_action.assert_not_called()
|
||||
mock_runtime.write.assert_not_called()
|
||||
|
||||
def test_maybe_setup_git_hooks_mkdir_failure(self, mock_runtime):
|
||||
# Test failure to create git hooks directory
|
||||
def mock_run_action(action):
|
||||
if (
|
||||
isinstance(action, CmdRunAction)
|
||||
and action.command == 'mkdir -p .git/hooks'
|
||||
):
|
||||
return CmdOutputObservation(
|
||||
content='Permission denied',
|
||||
exit_code=1,
|
||||
command='mkdir -p .git/hooks',
|
||||
)
|
||||
return CmdOutputObservation(content='', exit_code=0, command=action.command)
|
||||
|
||||
mock_runtime.run_action.side_effect = mock_run_action
|
||||
|
||||
Runtime.maybe_setup_git_hooks(mock_runtime)
|
||||
|
||||
# Verify that the runtime tried to create the git hooks directory
|
||||
assert mock_runtime.run_action.called
|
||||
|
||||
# Verify that the runtime logged an error
|
||||
mock_runtime.log.assert_called_with(
|
||||
'error', 'Failed to create git hooks directory: Permission denied'
|
||||
)
|
||||
|
||||
# Verify that no other actions were taken
|
||||
mock_runtime.write.assert_not_called()
|
||||
|
||||
def test_maybe_setup_git_hooks_with_existing_hook(self, mock_runtime):
|
||||
# Test when there's an existing pre-commit hook
|
||||
def mock_read(action):
|
||||
if action.path == '.openhands/pre-commit.sh':
|
||||
return FileReadObservation(
|
||||
content="#!/bin/bash\necho 'Test pre-commit hook'\nexit 0",
|
||||
path='.openhands/pre-commit.sh',
|
||||
)
|
||||
elif action.path == '.git/hooks/pre-commit':
|
||||
# Simulate existing pre-commit hook
|
||||
return FileReadObservation(
|
||||
content="#!/bin/bash\necho 'Existing hook'\nexit 0",
|
||||
path='.git/hooks/pre-commit',
|
||||
)
|
||||
return ErrorObservation(content='Unexpected path')
|
||||
|
||||
mock_runtime.read.side_effect = mock_read
|
||||
|
||||
Runtime.maybe_setup_git_hooks(mock_runtime)
|
||||
|
||||
# Verify that the runtime tried to read both scripts
|
||||
assert len(mock_runtime.read.call_args_list) >= 2
|
||||
|
||||
# Verify that the runtime preserved the existing hook
|
||||
assert mock_runtime.log.call_args_list[0] == call(
|
||||
'info', 'Preserving existing pre-commit hook'
|
||||
)
|
||||
|
||||
# Verify that the runtime moved the existing hook
|
||||
move_calls = [
|
||||
call
|
||||
for call in mock_runtime.run_action.call_args_list
|
||||
if isinstance(call[0][0], CmdRunAction) and 'mv' in call[0][0].command
|
||||
]
|
||||
assert len(move_calls) > 0
|
||||
|
||||
# Verify that the runtime wrote the new pre-commit hook
|
||||
assert mock_runtime.write.called
|
||||
|
||||
# Verify that the runtime logged success
|
||||
assert mock_runtime.log.call_args_list[-1] == call(
|
||||
'info', 'Git pre-commit hook installed successfully'
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user