Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dev): add hooks setup and pre-push hook with black formatter #101

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ test = [
"pytest>=8.2.2",
"pytest-cov>=5.0.0",
]
format = [
"black>=24.10.0",
]

[project.urls]
"Homepage" = "https://github.com/jahwag/claudesync"
Expand Down
77 changes: 77 additions & 0 deletions src/claudesync/cli/hook_templates/pre_commit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach didn’t work on Windows because only the python alias is mapped, leading to the error:
/usr/bin/env: 'python3': No such file or directory.

Since this issue could affect a number of users, it would be more robust to use #!/usr/bin/env python in the shebang for better cross-platform compatibility. To ensure the script runs with Python 3, you can add a runtime version check like this:

import sys
if sys.version_info.major < 3:
    print("Error: This script requires Python 3. Please install and configure it.")
    sys.exit(1)

This ensures that the script will fail gracefully if the system default python points to Python 2. It also avoids requiring users to set up the python3 alias, which may not be available by default on Windows.

import os
import subprocess
import sys


def get_changed_files() -> list[str]:
"""Get list of Python files that are staged for commit."""
try:
cmd = ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR"]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
files = result.stdout.strip().split("\n")
return [f for f in files if f.endswith(".py") and os.path.exists(f)]
except subprocess.CalledProcessError:
print("Failed to get changed files")
return []


def format_files(files: list[str]) -> tuple[bool, list[str]]:
"""Format the given files using black."""
if not files:
return True, []

formatted_files = []
try:
subprocess.run(["black", "--version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print(
"Error: black is not installed. Please install it with: pip install black"
)
return False, []

for file_path in files:
try:
subprocess.run(
["black", "--quiet", file_path], capture_output=True, check=True
)
formatted_files.append(file_path)
except subprocess.CalledProcessError as e:
print(f"Failed to format {file_path}: {e}")
return False, formatted_files

return True, formatted_files


def main():
files = get_changed_files()
if not files:
print("No Python files to format")
sys.exit(0)

print("Formatting Python files with black...")
success, formatted_files = format_files(files)

if formatted_files:
print("\nFormatted files:")
for file in formatted_files:
print(f" - {file}")

# Re-stage formatted files
try:
subprocess.run(["git", "add"] + formatted_files, check=True)
print("\nFormatted files have been re-staged")
except subprocess.CalledProcessError:
print("Failed to re-stage formatted files")
sys.exit(1)

if not success:
print("\nSome files could not be formatted")
sys.exit(1)

print("\nAll files formatted successfully")
sys.exit(0)


if __name__ == "__main__":
main()
90 changes: 90 additions & 0 deletions src/claudesync/cli/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import os
import shutil
import stat
import subprocess
from pathlib import Path

import click

SUPPORTED_GIT_HOOKS = [
"pre-commit",
]


@click.group()
def hooks():
"""Manage Git hooks for the project."""
pass


@hooks.command()
def install():
"""Install Git hooks for automatic code formatting."""
project_root = find_git_root()
if not project_root:
click.echo("Error: Not a git repository (or any of the parent directories)")
return

hooks_dir = project_root / ".git" / "hooks"
if not hooks_dir.exists():
click.echo(f"Creating hooks directory: {hooks_dir}")
hooks_dir.mkdir(parents=True, exist_ok=True)

for hook in SUPPORTED_GIT_HOOKS:
copy_hook(hooks_dir, hook)

click.echo("\nHook installation complete!")


@hooks.command()
def list():
"""List available Git hooks."""
click.echo("Available hooks:")
for hook in SUPPORTED_GIT_HOOKS:
click.echo(f" - {hook}")


def copy_hook(hooks_dir, hook_name):
"""
Copy a specific hook from templates to git hooks directory.

Args:
hooks_dir: Path to the .git/hooks directory
hook_name: Name of the hook to install
"""
hook_path = hooks_dir / hook_name
module_dir = Path(__file__).parent
template_name = f"{hook_name.replace('-', '_')}.py"
hook_template = module_dir / "hook_templates" / template_name

if not hook_template.exists():
click.echo(f"Warning: Template for {hook_name} not found at {hook_template}")
return

try:
shutil.copy2(hook_template, hook_path)

st = os.stat(hook_path)
os.chmod(hook_path, st.st_mode | stat.S_IEXEC)

click.echo(f"Successfully installed {hook_name} hook")
except Exception as e:
click.echo(f"Error installing {hook_name} hook: {e}")


def find_git_root():
"""Find the root directory of the Git repository"""
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
check=True,
)
return Path(result.stdout.strip())
except subprocess.CalledProcessError:
return None


if __name__ == "__main__":
install()
2 changes: 2 additions & 0 deletions src/claudesync/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .project import project
from .sync import schedule
from .config import config
from .hooks import hooks
import logging

logging.basicConfig(
Expand Down Expand Up @@ -213,6 +214,7 @@ def sync_submodule(provider, config, submodule, category):
cli.add_command(schedule)
cli.add_command(config)
cli.add_command(chat)
cli.add_command(hooks)

if __name__ == "__main__":
cli()
Loading