Skip to content

Commit a3a7f4e

Browse files
committed
feat(gitguard): improve repository detection and installation script
1 parent 9bd171a commit a3a7f4e

File tree

3 files changed

+198
-74
lines changed

3 files changed

+198
-74
lines changed

packages/gitguard/README.md

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,26 @@ cd packages/gitguard
4848

4949
## Features
5050

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

6672
## Security Features
6773

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

8995
1. When you create a commit, GitGuard:
96+
- Detects repository type (monorepo vs standard)
9097
- Analyzes your staged changes
9198
- Performs security checks for secrets and sensitive files
92-
- Warns about multi-package changes
99+
- Suggests appropriate commit structure based on repository type
93100
- Offers AI suggestions for commit messages
94-
2. Security checks:
95-
- Blocks commits containing detected secrets
96-
- Warns about newly added sensitive files
97-
- Provides specific remediation steps
98-
3. For multi-package changes:
99-
- Warns about atomic commit violations
100-
- Suggests splitting the commit
101-
- Adds "Affected packages" section
101+
102+
2. Repository-specific behavior:
103+
- Monorepo:
104+
- Enforces package scopes
105+
- Warns about cross-package changes
106+
- Adds "Affected packages" section for multi-package commits
107+
- Standard Repository:
108+
- Makes scopes optional
109+
- Groups changes by directory type (src, test, docs, etc.)
110+
- Focuses on change type and description clarity
102111

103112
## Example Usage
104113

105114
```bash
106-
# Regular commit
115+
# Monorepo commit
107116
git commit -m "update login form"
108117
# GitGuard will transform to: feat(auth): update login form
109118

110-
# Commit with security issues
111-
git commit -m "add config"
112-
# GitGuard will detect secrets or sensitive files and:
113-
# - Block the commit if secrets are found
114-
# - Warn about sensitive files and suggest .gitignore
119+
# Standard repo commit
120+
git commit -m "update login form"
121+
# GitGuard will transform to: feat: update login form
115122

116-
# Multi-package changes
123+
# Monorepo multi-package changes
117124
git commit -m "update theme colors"
118125
# GitGuard will warn about multiple packages and suggest:
119126
# style(design-system): update theme colors
120127
#
121128
# Affected packages:
122129
# - @siteed/design-system
123130
# - @siteed/mobile-components
131+
132+
# Standard repo complex changes
133+
git commit -m "update authentication"
134+
# GitGuard will suggest:
135+
# feat: update authentication
136+
#
137+
# Changes:
138+
# Source:
139+
# • src/auth/login.ts
140+
# • src/auth/session.ts
141+
# Tests:
142+
# • tests/auth/login.test.ts
124143
```
125144

126145
## Testing Security Features

packages/gitguard/gitguard-prepare.py

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ def group_files_by_type(files: List[str]) -> Dict[str, List[str]]:
217217
def enhance_ai_prompt(packages: List[Dict], original_msg: str) -> str:
218218
"""Generate detailed AI prompt based on commit complexity analysis."""
219219
complexity = calculate_commit_complexity(packages)
220+
is_mono = is_monorepo()
220221

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

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

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

249252
for pkg in analysis["packages"]:
250-
prompt += f"\n\n📦 {pkg['name']} ({pkg['scope']})"
253+
if is_mono:
254+
prompt += f"\n\n📦 {pkg['name']}" + (f" ({pkg['scope']})" if pkg['scope'] else "")
255+
else:
256+
prompt += f"\n\nDirectory: {pkg['name']}"
257+
251258
for file_type, files in pkg["files_by_type"].items():
252259
prompt += f"\n{file_type}:"
253260
for file in files:
@@ -269,11 +276,14 @@ def enhance_ai_prompt(packages: List[Dict], original_msg: str) -> str:
269276
"message": "complete commit message",
270277
"explanation": "reasoning",
271278
"type": "commit type",
272-
"scope": "scope",
279+
"scope": "{'scope (required for monorepo)' if is_mono else 'scope (optional)'}",
273280
"description": "title description"
274281
}}
275282
]
276-
}}"""
283+
}}
284+
285+
{'Note: This is a monorepo, so package scope is required.' if is_mono else 'Note: This is a standard repository, so scope is optional.'}
286+
"""
277287

278288
return prompt
279289

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

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

449-
# Calculate complexity
450458
complexity = calculate_commit_complexity(packages)
451459

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

459-
# For simple commits, just return the title
467+
# For simple commits
460468
if not complexity["needs_structure"]:
461-
return f"{commit_info['type']}({commit_info['scope']}): {commit_info['description']}"
469+
# Only include scope if it exists (for monorepo) or is explicitly set
470+
if commit_info.get('scope'):
471+
return f"{commit_info['type']}({commit_info['scope']}): {commit_info['description']}"
472+
return f"{commit_info['type']}: {commit_info['description']}"
462473

463-
# For complex commits, use structured format
474+
# For complex commits
464475
return create_structured_commit(commit_info, packages)
465476

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

474484
# Start with the commit title
475-
message_parts = [
476-
f"{commit_info['type']}({commit_info['scope']}): {description}"
477-
]
485+
if commit_info.get('scope'):
486+
message_parts = [f"{commit_info['type']}({commit_info['scope']}): {description}"]
487+
else:
488+
message_parts = [f"{commit_info['type']}: {description}"]
478489

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

515-
# ... [previous code remains the same until get_changed_packages]
526+
def is_monorepo() -> bool:
527+
"""Detect if the current repository is a monorepo."""
528+
try:
529+
git_root = Path(check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip())
530+
531+
# Common monorepo indicators
532+
monorepo_indicators = [
533+
git_root / "packages",
534+
git_root / "apps",
535+
git_root / "libs",
536+
git_root / "services"
537+
]
538+
539+
# Check for package.json with workspaces
540+
package_json = git_root / "package.json"
541+
if package_json.exists():
542+
try:
543+
data = json.loads(package_json.read_text())
544+
if "workspaces" in data:
545+
return True
546+
except json.JSONDecodeError:
547+
pass
548+
549+
# Check for common monorepo directories
550+
return any(indicator.is_dir() for indicator in monorepo_indicators)
551+
552+
except Exception as e:
553+
if Config().get("debug"):
554+
print(f"Error detecting repository type: {e}")
555+
return False
516556

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

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

583+
is_mono = is_monorepo()
545584
packages = {}
585+
546586
for file in changed_files:
547587
if not file:
548588
continue
549589

550-
if file.startswith("packages/"):
590+
if is_mono and file.startswith("packages/"):
551591
parts = file.split("/")
552592
if len(parts) > 1:
553593
pkg_path = f"packages/{parts[1]}"
554594
if pkg_path not in packages:
555595
packages[pkg_path] = []
556596
packages[pkg_path].append(file)
557597
else:
558-
if "root" not in packages:
559-
packages["root"] = []
560-
packages["root"].append(file)
598+
# For standard repos, group by directory type
599+
path_parts = Path(file).parts
600+
if not path_parts:
601+
continue
602+
603+
# Determine appropriate grouping based on file type/location
604+
if path_parts[0] in {"src", "test", "docs", "scripts"}:
605+
group = path_parts[0]
606+
else:
607+
group = "root"
608+
609+
if group not in packages:
610+
packages[group] = []
611+
packages[group].append(file)
561612

562613
results = []
563614
for pkg_path, files in packages.items():
564-
if pkg_path == "root":
565-
scope = name = "root"
566-
else:
567-
pkg_name = get_package_json_name(Path(pkg_path))
568-
if pkg_name:
569-
name = pkg_name
570-
scope = pkg_name.split("/")[-1]
615+
if is_mono:
616+
if pkg_path == "root":
617+
scope = name = "root"
571618
else:
572-
name = scope = pkg_path.split("/")[-1]
619+
pkg_name = get_package_json_name(Path(pkg_path))
620+
if pkg_name:
621+
name = pkg_name
622+
scope = pkg_name.split("/")[-1]
623+
else:
624+
name = scope = pkg_path.split("/")[-1]
625+
else:
626+
# For standard repos, scope is optional
627+
name = pkg_path
628+
scope = None if pkg_path == "root" else pkg_path
573629

574630
results.append({
575631
"name": name,

0 commit comments

Comments
 (0)