Skip to content

Commit

Permalink
feat(gitguard): improve repository detection and installation script
Browse files Browse the repository at this point in the history
  • Loading branch information
abretonc7s committed Nov 1, 2024
1 parent 9bd171a commit a3a7f4e
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 74 deletions.
73 changes: 46 additions & 27 deletions packages/gitguard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,26 @@ cd packages/gitguard

## Features

- 🎯 **Automatic Scope Detection**: Automatically detects the package scope based on changed files
- 🎯 **Smart Repository Detection**:
- Automatically detects monorepo vs standard repository structure
- Adapts commit message format accordingly
- 🤖 **Multi-Provider AI Suggestions**: Offers intelligent commit message suggestions using:
- Azure OpenAI (with fallback model support)
- Local Ollama models
- 📦 **Monorepo Awareness**: Detects changes across multiple packages and suggests appropriate formatting
-**Conventional Commits**: Enforces conventional commit format (`type(scope): description`)
- 🔍 **Change Analysis**: Analyzes file changes to suggest appropriate commit types
- 🚨 **Multi-Package Warning**: Alerts when changes span multiple packages, encouraging atomic commits
- 🔒 **Security Checks**:
- Detects accidentally committed secrets and sensitive data
- Identifies problematic files (env files, keys, logs, etc.)
- Only warns about newly added problematic files
- Provides specific remediation steps
- Blocks commits containing secrets
- 📦 **Repository-Aware Formatting**:
- Monorepo: Enforces package scopes and detects cross-package changes
- Standard Repos: Uses conventional commits without forcing scopes
-**Conventional Commits**:
- Enforces conventional commit format
- Monorepo: `type(scope): description`
- Standard: `type: description` (scope optional)
- 🔍 **Smart Change Analysis**:
- Analyzes file changes to suggest appropriate commit types
- Groups changes by directory type in standard repos
- Groups by package in monorepos
- 🚨 **Change Cohesion Checks**:
- Monorepo: Alerts when changes span multiple packages
- Standard: Warns about changes across unrelated components

## Security Features

Expand All @@ -87,40 +93,53 @@ Warns about newly added sensitive files:
## How It Works

1. When you create a commit, GitGuard:
- Detects repository type (monorepo vs standard)
- Analyzes your staged changes
- Performs security checks for secrets and sensitive files
- Warns about multi-package changes
- Suggests appropriate commit structure based on repository type
- Offers AI suggestions for commit messages
2. Security checks:
- Blocks commits containing detected secrets
- Warns about newly added sensitive files
- Provides specific remediation steps
3. For multi-package changes:
- Warns about atomic commit violations
- Suggests splitting the commit
- Adds "Affected packages" section

2. Repository-specific behavior:
- Monorepo:
- Enforces package scopes
- Warns about cross-package changes
- Adds "Affected packages" section for multi-package commits
- Standard Repository:
- Makes scopes optional
- Groups changes by directory type (src, test, docs, etc.)
- Focuses on change type and description clarity

## Example Usage

```bash
# Regular commit
# Monorepo commit
git commit -m "update login form"
# GitGuard will transform to: feat(auth): update login form

# Commit with security issues
git commit -m "add config"
# GitGuard will detect secrets or sensitive files and:
# - Block the commit if secrets are found
# - Warn about sensitive files and suggest .gitignore
# Standard repo commit
git commit -m "update login form"
# GitGuard will transform to: feat: update login form

# Multi-package changes
# Monorepo multi-package changes
git commit -m "update theme colors"
# GitGuard will warn about multiple packages and suggest:
# style(design-system): update theme colors
#
# Affected packages:
# - @siteed/design-system
# - @siteed/mobile-components

# Standard repo complex changes
git commit -m "update authentication"
# GitGuard will suggest:
# feat: update authentication
#
# Changes:
# Source:
# • src/auth/login.ts
# • src/auth/session.ts
# Tests:
# • tests/auth/login.test.ts
```

## Testing Security Features
Expand Down
112 changes: 84 additions & 28 deletions packages/gitguard/gitguard-prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ def group_files_by_type(files: List[str]) -> Dict[str, List[str]]:
def enhance_ai_prompt(packages: List[Dict], original_msg: str) -> str:
"""Generate detailed AI prompt based on commit complexity analysis."""
complexity = calculate_commit_complexity(packages)
is_mono = is_monorepo()

try:
diff = check_output(["git", "diff", "--cached"]).decode("utf-8")
Expand All @@ -227,6 +228,7 @@ def enhance_ai_prompt(packages: List[Dict], original_msg: str) -> str:
analysis = {
"complexity_score": complexity["score"],
"complexity_reasons": complexity["reasons"],
"repository_type": "monorepo" if is_mono else "standard",
"packages": []
}

Expand All @@ -240,14 +242,19 @@ def enhance_ai_prompt(packages: List[Dict], original_msg: str) -> str:

prompt = f"""Analyze the following git changes and suggest a commit message.
Repository Type: {analysis['repository_type']}
Complexity Analysis:
- Score: {complexity['score']} (threshold for structured format: 5)
- Factors: {', '.join(complexity['reasons'])}
Changed Packages:"""
Changed Files:"""

for pkg in analysis["packages"]:
prompt += f"\n\n📦 {pkg['name']} ({pkg['scope']})"
if is_mono:
prompt += f"\n\n📦 {pkg['name']}" + (f" ({pkg['scope']})" if pkg['scope'] else "")
else:
prompt += f"\n\nDirectory: {pkg['name']}"

for file_type, files in pkg["files_by_type"].items():
prompt += f"\n{file_type}:"
for file in files:
Expand All @@ -269,11 +276,14 @@ def enhance_ai_prompt(packages: List[Dict], original_msg: str) -> str:
"message": "complete commit message",
"explanation": "reasoning",
"type": "commit type",
"scope": "scope",
"scope": "{'scope (required for monorepo)' if is_mono else 'scope (optional)'}",
"description": "title description"
}}
]
}}"""
}}
{'Note: This is a monorepo, so package scope is required.' if is_mono else 'Note: This is a standard repository, so scope is optional.'}
"""

return prompt

Expand Down Expand Up @@ -439,14 +449,12 @@ def get_ai_suggestion(prompt: str, original_message: str) -> Optional[List[Dict[

def create_commit_message(commit_info: Dict[str, Any], packages: List[Dict[str, Any]]) -> str:
"""Create appropriate commit message based on complexity."""
# Clean the description to remove any existing type prefix
description = commit_info['description']
type_pattern = r'^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\([^)]+\))?:\s*'
if re.match(type_pattern, description):
description = re.sub(type_pattern, '', description)
commit_info['description'] = description.strip()

# Calculate complexity
complexity = calculate_commit_complexity(packages)

if Config().get("debug"):
Expand All @@ -456,25 +464,28 @@ def create_commit_message(commit_info: Dict[str, Any], packages: List[Dict[str,
for reason in complexity["reasons"]:
print(f"- {reason}")

# For simple commits, just return the title
# For simple commits
if not complexity["needs_structure"]:
return f"{commit_info['type']}({commit_info['scope']}): {commit_info['description']}"
# Only include scope if it exists (for monorepo) or is explicitly set
if commit_info.get('scope'):
return f"{commit_info['type']}({commit_info['scope']}): {commit_info['description']}"
return f"{commit_info['type']}: {commit_info['description']}"

# For complex commits, use structured format
# For complex commits
return create_structured_commit(commit_info, packages)

def create_structured_commit(commit_info: Dict[str, Any], packages: List[Dict[str, Any]]) -> str:
"""Create a structured commit message for complex changes."""
# Clean the description to remove any existing type prefix
description = commit_info['description']
type_pattern = r'^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\([^)]+\))?:\s*'
if re.match(type_pattern, description):
description = re.sub(type_pattern, '', description)

# Start with the commit title
message_parts = [
f"{commit_info['type']}({commit_info['scope']}): {description}"
]
if commit_info.get('scope'):
message_parts = [f"{commit_info['type']}({commit_info['scope']}): {description}"]
else:
message_parts = [f"{commit_info['type']}: {description}"]

# Add a blank line after title
message_parts.append("")
Expand Down Expand Up @@ -512,16 +523,43 @@ def get_package_json_name(package_path: Path) -> Optional[str]:
return None
return None

# ... [previous code remains the same until get_changed_packages]
def is_monorepo() -> bool:
"""Detect if the current repository is a monorepo."""
try:
git_root = Path(check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip())

# Common monorepo indicators
monorepo_indicators = [
git_root / "packages",
git_root / "apps",
git_root / "libs",
git_root / "services"
]

# Check for package.json with workspaces
package_json = git_root / "package.json"
if package_json.exists():
try:
data = json.loads(package_json.read_text())
if "workspaces" in data:
return True
except json.JSONDecodeError:
pass

# Check for common monorepo directories
return any(indicator.is_dir() for indicator in monorepo_indicators)

except Exception as e:
if Config().get("debug"):
print(f"Error detecting repository type: {e}")
return False

def get_changed_packages() -> List[Dict]:
"""Get all packages with changes in the current commit."""
try:
# Get git root directory
git_root = Path(check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip())
current_dir = Path.cwd()

# Get relative path from current directory to git root
try:
rel_path = current_dir.relative_to(git_root)
except ValueError:
Expand All @@ -542,34 +580,52 @@ def get_changed_packages() -> List[Dict]:
print(f"Error getting changed files: {e}")
return []

is_mono = is_monorepo()
packages = {}

for file in changed_files:
if not file:
continue

if file.startswith("packages/"):
if is_mono and file.startswith("packages/"):
parts = file.split("/")
if len(parts) > 1:
pkg_path = f"packages/{parts[1]}"
if pkg_path not in packages:
packages[pkg_path] = []
packages[pkg_path].append(file)
else:
if "root" not in packages:
packages["root"] = []
packages["root"].append(file)
# For standard repos, group by directory type
path_parts = Path(file).parts
if not path_parts:
continue

# Determine appropriate grouping based on file type/location
if path_parts[0] in {"src", "test", "docs", "scripts"}:
group = path_parts[0]
else:
group = "root"

if group not in packages:
packages[group] = []
packages[group].append(file)

results = []
for pkg_path, files in packages.items():
if pkg_path == "root":
scope = name = "root"
else:
pkg_name = get_package_json_name(Path(pkg_path))
if pkg_name:
name = pkg_name
scope = pkg_name.split("/")[-1]
if is_mono:
if pkg_path == "root":
scope = name = "root"
else:
name = scope = pkg_path.split("/")[-1]
pkg_name = get_package_json_name(Path(pkg_path))
if pkg_name:
name = pkg_name
scope = pkg_name.split("/")[-1]
else:
name = scope = pkg_path.split("/")[-1]
else:
# For standard repos, scope is optional
name = pkg_path
scope = None if pkg_path == "root" else pkg_path

results.append({
"name": name,
Expand Down
Loading

0 comments on commit a3a7f4e

Please sign in to comment.