Files
DevGen/devgen/utils.py
T
2025-12-12 11:10:30 +05:45

199 lines
5.4 KiB
Python

import logging
import re
import subprocess
import time
from pathlib import Path
from typing import Any, Dict, Optional
import yaml
from jinja2 import Environment, FileSystemLoader
def ensure_log_directory() -> Path:
log_dir = Path.home() / ".cache" / "devgen"
log_dir.mkdir(parents=True, exist_ok=True)
return log_dir
def get_main_log_path() -> Path:
return ensure_log_directory() / "devgen.log"
def get_commit_dry_run_path() -> Path:
return ensure_log_directory() / "commit_dry_run.md"
def is_file_recent(file_path: Path | str, max_age_minutes: int = 120) -> bool:
path = Path(file_path)
if not path.exists():
return False
return (time.time() - path.stat().st_mtime) <= max_age_minutes * 60
def sanitize_ai_commit_message(raw_text: str) -> str:
lines = raw_text.strip().split("\n")
cleaned_lines = []
in_block = False
# Regex for conventional commit header
header_pattern = re.compile(
r"^(feat|fix|chore|refactor|docs|style|test|build|ci)(\(.*\))?!?: .*"
)
for line in lines:
stripped = line.strip()
if in_block:
if header_pattern.match(stripped) or "**Sponsor**" in line:
break
cleaned_lines.append(line)
elif header_pattern.match(stripped):
in_block = True
cleaned_lines.append(line)
return "\n".join(cleaned_lines).strip() if cleaned_lines else ""
def parse_markdown_sections(
filepath: Path | str, marker_pattern: str
) -> dict[str, str]:
path = Path(filepath)
if not path.exists():
return {}
with path.open(encoding="utf-8") as f:
content = f.read()
results = {}
matches = re.findall(marker_pattern, content, re.DOTALL)
for key, value in matches:
results[key] = value.strip()
return results
def extract_commit_messages(filepath: Path | str) -> dict[str, str]:
pattern = r"## Group: `(.*?)`\s*```md\n(.*?)\n```"
return parse_markdown_sections(filepath, pattern)
def configure_logger(
name: str = "devgen", log_file: Optional[Path | str] = None
) -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
if logger.hasHandlers():
logger.handlers.clear()
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Console handler
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)
# File handler
if log_file:
path = Path(log_file)
path.parent.mkdir(parents=True, exist_ok=True)
fh = logging.FileHandler(path, mode="w", encoding="utf-8")
fh.setFormatter(formatter)
logger.addHandler(fh)
return logger
def get_git_staged_files() -> list[str]:
try:
res = subprocess.run(
["git", "diff", "--name-only", "--cached"],
capture_output=True,
text=True,
check=True,
)
return [f for f in res.stdout.splitlines() if f.strip()]
except subprocess.CalledProcessError:
return []
def read_file_content(filepath: Path | str) -> Optional[str]:
path = Path(filepath)
if path.exists():
return path.read_text(encoding="utf-8")
return None
def delete_file(filepath: Path | str) -> bool:
path = Path(filepath)
if path.exists():
path.unlink()
return True
return False
def load_template_env(sub_dir: str) -> Environment:
template_dir = Path(__file__).parent / "prompt_templates" / sub_dir
return Environment(loader=FileSystemLoader(template_dir))
def load_config() -> Dict[str, Any]:
config_path = Path.home() / ".devgen.yaml"
if not config_path.exists():
default_config = {
"provider": "gemini",
"model": "gemini-2.5-flash",
"api_key": "",
"emoji": True,
}
try:
with config_path.open("w", encoding="utf-8") as f:
yaml.dump(default_config, f, default_flow_style=False)
# We don't print here to avoid noise during normal execution
except Exception as e:
print(f"Warning: Failed to create default config at {config_path}: {e}")
return {}
try:
with config_path.open("r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
except Exception as e:
print(f"Warning: Failed to load config from {config_path}: {e}")
return {}
__all__ = [
"ensure_log_directory",
"get_main_log_path",
"get_commit_dry_run_path",
"is_file_recent",
"sanitize_ai_commit_message",
"extract_commit_messages",
"configure_logger",
"get_git_staged_files",
"read_file_content",
"delete_file",
"load_template_env",
"load_config",
"get_questionary_style",
]
def get_questionary_style():
from questionary import Style
return Style(
[
("qmark", "fg:#673ab7 bold"), # Token.QuestionMark
("question", "bold"), # Token.Question
("answer", "fg:#f44336 bold"), # Token.Answer
("pointer", "fg:#673ab7 bold"), # Token.Pointer
("highlighted", "fg:#673ab7 bold"), # Token.Selected
("selected", "fg:#cc5454"), # Token.SelectedItem
("separator", "fg:#cc5454"), # Token.Separator
("instruction", ""), # Token.Instruction
("text", ""), # Token.Text
("disabled", "fg:#858585 italic"), # Token.Disabled
]
)