feat(hooks): add git hooks management CLI

This commit is contained in:
sankalp committed 2025-08-25 14:17:12 +05:45
1 parent b09b5f676e
commit 0036e47968
24 files changed
+1221 -531

No files matched your search

+1 -5
View File
@@ -6,8 +6,4 @@ from .cli import cli
from .generator import GitignoreGenerator
from .detector import ProjectDetector
__all__ = [
"cli",
"GitignoreGenerator",
"ProjectDetector"
]
__all__ = ["cli", "GitignoreGenerator", "ProjectDetector"]
+22 -13
View File
@@ -1,6 +1,7 @@
"""
Gitignore generation CLI.
"""
import os
import click
from rich.console import Console
@@ -11,11 +12,13 @@ from .detector import ProjectDetector
console = Console()
@click.group()
def cli():
"""Generate .gitignore files."""
pass
@cli.command()
@click.option("--directory", "-d", default=".", help="Directory to scan")
@click.option("--output", "-o", default=".gitignore", help="Output file path")
@@ -24,33 +27,38 @@ def auto(directory: str, output: str):
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console
console=console,
) as progress:
# Initialize services
progress.add_task("Initializing...", total=None)
detector = ProjectDetector(directory)
generator = GitignoreGenerator()
# Detect project types
progress.add_task("Detecting project types...", total=None)
detected_types = detector.detect_project_types()
if not detected_types:
console.print("[yellow]No project types detected. Using generic .gitignore.")
console.print(
"[yellow]No project types detected. Using generic .gitignore."
)
else:
console.print(f"\n[green]Detected project types:[/] {', '.join(detected_types)}")
console.print(
f"\n[green]Detected project types:[/] {', '.join(detected_types)}"
)
# Check if output file exists
if os.path.exists(output):
if not Confirm.ask(f"\n[yellow]File {output} already exists. Overwrite?"):
return
# Generate .gitignore
progress.add_task("Generating .gitignore...", total=None)
generator.update_gitignore_file(detected_types, output)
console.print(f"\n[green]Generated .gitignore at {output}")
@cli.command()
@click.argument("types", nargs=-1, required=True)
@click.option("--output", "-o", default=".gitignore", help="Output file path")
@@ -59,26 +67,27 @@ def generate(types: tuple, output: str):
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console
console=console,
) as progress:
# Initialize services
progress.add_task("Initializing...", total=None)
generator = GitignoreGenerator()
# Check if output file exists
if os.path.exists(output):
if not Confirm.ask(f"\n[yellow]File {output} already exists. Overwrite?"):
return
# Generate .gitignore
progress.add_task("Generating .gitignore...", total=None)
generator.update_gitignore_file(list(types), output)
console.print(f"\n[green]Generated .gitignore at {output}")
# Add command aliases
cli.add_command(auto, name="a")
cli.add_command(generate, name="g")
if __name__ == "__main__":
cli()
cli()
+21 -29
View File
@@ -1,12 +1,14 @@
"""
Project type detection for gitignore generation.
"""
import os
from typing import List, Dict, Set
class ProjectDetector:
"""Detect project types based on file patterns."""
# File patterns for different project types
PROJECT_PATTERNS = {
# Languages
@@ -22,7 +24,6 @@ class ProjectDetector:
"csharp": ["*.cs", "*.csproj", "*.sln"],
"swift": ["*.swift", "*.xcodeproj"],
"objective-c": ["*.m", "*.h", "*.mm"],
# Frameworks
"django": ["manage.py", "wsgi.py", "asgi.py"],
"flask": ["app.py", "flask_app.py"],
@@ -36,7 +37,6 @@ class ProjectDetector:
"express": ["app.js", "server.js"],
"next": ["next.config.js"],
"nuxt": ["nuxt.config.js"],
# Build Tools
"maven": ["pom.xml"],
"gradle": ["build.gradle", "settings.gradle"],
@@ -46,7 +46,6 @@ class ProjectDetector:
"poetry": ["pyproject.toml"],
"cargo": ["Cargo.toml"],
"composer": ["composer.json"],
# IDEs and Editors
"vscode": [".vscode/settings.json"],
"intellij": [".idea/workspace.xml"],
@@ -55,107 +54,100 @@ class ProjectDetector:
"emacs": [".emacs", ".emacs.d/"],
"sublime": ["*.sublime-project", "*.sublime-workspace"],
"atom": [".atom/"],
# Operating Systems
"windows": ["*.exe", "*.dll", "*.sys"],
"macos": [".DS_Store", "*.app"],
"linux": ["*.so", "*.o"],
# Testing Frameworks
"jest": ["jest.config.js"],
"pytest": ["pytest.ini", "conftest.py"],
"junit": ["junit.xml"],
"mocha": ["mocha.opts"],
# Documentation
"sphinx": ["docs/conf.py"],
"mkdocs": ["mkdocs.yml"],
"docusaurus": ["docusaurus.config.js"],
# Databases
"postgresql": ["*.sql", "*.dump"],
"mysql": ["*.sql", "*.dump"],
"mongodb": ["*.bson"],
"sqlite": ["*.db", "*.sqlite"],
# Cloud Platforms
"aws": [".aws/", "aws.json"],
"azure": ["azure.yaml", "azure.json"],
"gcp": ["gcloud.json"],
# Containerization
"docker": ["Dockerfile", "docker-compose.yml"],
"kubernetes": ["k8s/", "*.yaml"],
# CI/CD
"github-actions": [".github/workflows/"],
"gitlab-ci": [".gitlab-ci.yml"],
"jenkins": ["Jenkinsfile"],
"travis": [".travis.yml"],
"circleci": [".circleci/config.yml"],
# Static Site Generators
"jekyll": ["_config.yml"],
"hugo": ["config.toml", "config.yaml"],
"gatsby": ["gatsby-config.js"],
# Game Development
"unity": ["*.unity", "*.unityproj"],
"unreal": ["*.uproject"],
# Machine Learning
"tensorflow": ["*.pb", "*.h5"],
"pytorch": ["*.pt", "*.pth"],
"scikit-learn": ["*.joblib"]
"scikit-learn": ["*.joblib"],
}
def __init__(self, directory: str = "."):
"""Initialize project detector.
Args:
directory: Directory to scan
"""
self.directory = directory
def detect_project_types(self) -> List[str]:
"""Detect project types in the directory.
Returns:
List of detected project types
"""
detected_types = set()
# Scan directory for files
for root, _, files in os.walk(self.directory):
for file in files:
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, self.directory)
# Check each project type's patterns
for type_, patterns in self.PROJECT_PATTERNS.items():
if any(self._matches_pattern(rel_path, pattern) for pattern in patterns):
if any(
self._matches_pattern(rel_path, pattern) for pattern in patterns
):
detected_types.add(type_)
return sorted(list(detected_types))
def _matches_pattern(self, path: str, pattern: str) -> bool:
"""Check if a path matches a pattern.
Args:
path: Path to check
pattern: Pattern to match
Returns:
True if path matches pattern
"""
# Handle directory patterns
if pattern.endswith("/"):
return path.startswith(pattern[:-1])
# Handle wildcard patterns
if "*" in pattern:
import fnmatch
return fnmatch.fnmatch(path, pattern)
# Handle exact matches
return path == pattern
return path == pattern
+16 -14
View File
@@ -1,47 +1,49 @@
"""
Gitignore generator using gitignore.io API.
"""
import os
import requests
from typing import List, Dict
class GitignoreGenerator:
"""Gitignore generator using gitignore.io API."""
def __init__(self):
"""Initialize gitignore generator."""
self.api_url = "https://www.toptal.com/developers/gitignore/api"
def generate_gitignore(self, types: List[str]) -> str:
"""Generate .gitignore content for specified types.
Args:
types: List of project types
Returns:
Generated .gitignore content
"""
if not types:
return self._get_generic_gitignore()
try:
# Get gitignore content from API
response = requests.get(f"{self.api_url}/{','.join(types)}")
response.raise_for_status()
# Add header
header = "# Generated by devtools gitignore generator\n"
header += f"# Types: {', '.join(types)}\n\n"
return header + response.text
except requests.RequestException:
# Fallback to generic gitignore if API fails
return self._get_generic_gitignore()
def _get_generic_gitignore(self) -> str:
"""Get a generic .gitignore template.
Returns:
Generic .gitignore content
"""
@@ -91,16 +93,16 @@ logs/
*.temp
*.bak
"""
def update_gitignore_file(self, types: List[str], output_file: str) -> None:
"""Generate and update a .gitignore file.
Args:
types: List of project types
output_file: Path to the .gitignore file
"""
content = self.generate_gitignore(types)
# Write to file
with open(output_file, "w") as f:
f.write(content)
f.write(content)