Skip to content

Commit 49629d6

Browse files
committed
feat: implement smart, project-aware gitignore generator with user permission control
1 parent b1d1135 commit 49629d6

3 files changed

Lines changed: 87 additions & 9 deletions

File tree

devguardian/server.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async def _run_sync(func, *args, **kwargs):
3636
from devguardian.tools.code_helper import explain_code, review_code, generate_code, improve_code
3737
from devguardian.tools.tdd import test_and_fix
3838
from devguardian.tools.github_review import review_pull_request
39-
from devguardian.tools.infra import dockerize, generate_ci
39+
from devguardian.tools.infra import dockerize, generate_ci, generate_gitignore
4040
from devguardian.tools.mass_refactor import mass_refactor
4141
from devguardian.tools.git_ops import (
4242
git_status,
@@ -245,6 +245,24 @@ async def list_tools() -> list[types.Tool]:
245245
"required": ["project_path"],
246246
},
247247
),
248+
types.Tool(
249+
name="generate_gitignore",
250+
description=(
251+
"🛡️ Smart .gitignore Generator: analyzes the project structure and "
252+
"generates a tailored .gitignore file via AI."
253+
),
254+
inputSchema={
255+
"type": "object",
256+
"properties": {
257+
"project_path": {"type": "string"},
258+
"include_env": {
259+
"type": "boolean",
260+
"description": "If true, explicitly include .env and credentials in the ignore list. (User Permission)",
261+
},
262+
},
263+
"required": ["project_path"],
264+
},
265+
),
248266
types.Tool(
249267
name="mass_refactor",
250268
description=(
@@ -455,6 +473,8 @@ def text(result) -> list[types.TextContent]:
455473
return text(await _run_sync(dockerize, **arguments))
456474
elif name == "generate_ci":
457475
return text(await _run_sync(generate_ci, **arguments))
476+
elif name == "generate_gitignore":
477+
return text(await _run_sync(generate_gitignore, **arguments))
458478
elif name == "mass_refactor":
459479
return text(await _run_sync(mass_refactor, **arguments))
460480

devguardian/tools/infra.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,30 @@ def generate_ci(project_path: str, deploy_target: str = "") -> str:
115115
f"```yaml\n{yaml_content[:1200]}\n```\n"
116116
+ ("*(truncated — see file for full content)*" if len(yaml_content) > 1200 else "")
117117
)
118+
119+
120+
def generate_gitignore(project_path: str, include_env: bool = False) -> str:
121+
"""
122+
Analyze the project and generate a tailored .gitignore.
123+
"""
124+
from devguardian.utils.security import generate_smart_gitignore
125+
126+
content = generate_smart_gitignore(project_path, include_env=include_env)
127+
root = Path(project_path)
128+
gitignore_path = root / ".gitignore"
129+
130+
# Backup if it exists
131+
if gitignore_path.exists():
132+
backup = gitignore_path.with_suffix(".gitignore.bak")
133+
gitignore_path.rename(backup)
134+
135+
gitignore_path.write_text(content, encoding="utf-8")
136+
137+
return (
138+
f"## 🛡️ Smart .gitignore Generated!\n\n"
139+
f"Project: `{project_path}`\n"
140+
f"- ✅ Tailored to project structure\n"
141+
f"- ✅ Sensitive file check: {'Enabled' if include_env else 'Manual review requested'}\n\n"
142+
f"```text\n{content[:800]}\n```\n"
143+
+ ("*(truncated — see file for full content)*" if len(content) > 800 else "")
144+
)

devguardian/utils/security.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,19 +225,49 @@ def scan_content_for_secrets(content: str, filename: str = "") -> list[str]:
225225

226226

227227
# ---------------------------------------------------------------------------
228-
# 4. .gitignore coverage checker
228+
# 4. .gitignore coverage checker & generator
229229
# ---------------------------------------------------------------------------
230230

231231

232+
def generate_smart_gitignore(project_path: str, include_env: bool = False) -> str:
233+
"""
234+
Generate a tailored .gitignore for the project using Gemini and local context.
235+
"""
236+
from devguardian.utils.file_reader import build_project_context
237+
from devguardian.utils.gemini_client import ask_gemini
238+
239+
ctx = build_project_context(project_path)
240+
env_msg = (
241+
"ALWAYS include .env and credentials patterns."
242+
if include_env
243+
else "DO NOT explicitly include .env unless the user confirms (ask for permission). "
244+
"Actually, for safety, suggest it but mark it with a comment."
245+
)
246+
247+
prompt = (
248+
f"{ctx}\n\n"
249+
"Generate a professional, tailored .gitignore for this project. "
250+
"1. Identify the programming language and frameworks.\n"
251+
"2. Include common ignore patterns for that stack (e.g., __pycache__, node_modules, .venv, etc.).\n"
252+
"3. Look at the file structure above and identify any specific large folders or temp files to ignore.\n"
253+
f"4. Regarding .env: {env_msg}\n"
254+
"\nReturn ONLY the content of the .gitignore file. No markdown fences, no explanations."
255+
)
256+
257+
system_instr = (
258+
"You are a DevOps architect. Generate a clean, well-organized .gitignore file. "
259+
"Group patterns by category (e.g., # IDEs, # Environment, # Build artifacts)."
260+
)
261+
262+
result = ask_gemini(prompt, system_instruction=system_instr)
263+
# Strip fences if present
264+
lines = [l for l in result.splitlines() if not l.strip().startswith("```")]
265+
return "\n".join(lines).strip()
266+
267+
232268
def check_gitignore(repo_path: str) -> dict:
233269
"""
234270
Check that .gitignore properly covers sensitive file patterns.
235-
236-
Returns:
237-
ok : bool — True if all critical patterns are covered
238-
covered : list — patterns that ARE in .gitignore
239-
missing : list — patterns that should be in .gitignore but aren't
240-
warnings : list — human-readable warnings
241271
"""
242272
root = Path(repo_path)
243273
gitignore_path = root / ".gitignore"
@@ -257,6 +287,7 @@ def check_gitignore(repo_path: str) -> dict:
257287
line.strip() for line in gitignore_content.splitlines() if line.strip() and not line.strip().startswith("#")
258288
}
259289

290+
# First check the high-priority "MUST IGNORE" list
260291
for pattern in _MUST_IGNORE:
261292
# Check exact match or wildcard coverage
262293
covered = (
@@ -272,7 +303,7 @@ def check_gitignore(repo_path: str) -> dict:
272303
if result["missing"]:
273304
result["ok"] = False
274305
result["warnings"].append(
275-
"These patterns are missing from .gitignore: " + ", ".join(f"`{p}`" for p in result["missing"])
306+
"These critical patterns are missing from .gitignore: " + ", ".join(f"`{p}`" for p in result["missing"])
276307
)
277308

278309
return result

0 commit comments

Comments
 (0)