Compare commits

..
17 Commits
Author SHA1 Message Date
sankalp cc94b53d48 chore(root): bump version to 0.2.4
- Updated version in `pyproject.toml` and `setup.py` to `0.2.4`
- Lockfile (`uv.lock`) updated to reflect dependency changes
2025-12-30 14:40:57 +05:45
sankalp b09de402cc feat(devgen): add max_groups config option 2025-12-25 09:36:05 +05:45
sankalp c121a7fa59 feat(devgen/modules): add interactive commit review and smart file grouping
- New Features:
  - Added `check` parameter to enable interactive commit message review
  - Implemented user confirmation/editing/abort workflow using questionary
  - Added smart file grouping with configurable max_groups limit
  - Files are now merged into parent directories when group count exceeds limit

- Enhancements:
  - Improved `group_files()` method with hierarchical merging logic
  - Added graceful KeyboardInterrupt handling during user review
  - Enhanced logging for group merging operations
```
2025-12-25 09:35:33 +05:45
sankalp 525aebadf4 feat(devgen/modules): enhance commit generation with manifest context and custom templates
- New Features:
  - Added `_get_manifest_context()` to extract project metadata from manifest files (pyproject.toml, package.json, etc.) for richer commit message context.
  - Added support for custom commit message templates via `custom_template` config.
  - Added emoji instruction toggle based on global `use_emoji` setting.

- Enhancements:
  - Improved error handling for git push failures with specific hints for "no upstream branch" scenarios.
  - Optimized manifest context extraction by truncating large files to avoid token bloat.
  - Refactored prompt generation to dynamically include manifest context and custom templates.
```
2025-12-25 00:04:54 +05:45
sankalp 1219c1ad81 feat(devgen/templates/commit): add context to commit message template
- New Features:
  - Added `{{ context }}` variable to the commit message template to provide additional context.
- Enhancements:
  - Removed the emoji usage line from the template to simplify the output.
```
2025-12-25 00:04:50 +05:45
sankalp cfa891dcde feat(devgen/cli): add config info command and improve undo commit message formatting 2025-12-25 00:04:48 +05:45
sankalp 286c46d2b5 feat(root): enhance commit generation with context-aware features
- Add shell completion support for bash/zsh/fish via `devgen --install-completion`
- Introduce `devgen commit undo` to revert the last AI-generated commit while preserving staged changes
- Implement context-aware commit generation by analyzing project manifests (`package.json`, `pyproject.toml`, etc.) and lock files for accurate dependency updates
- Add support for custom Jinja2 templates in `~/.devgen.yaml` with `custom_template` configuration
- Update documentation to reflect new features and provide template variable reference

This release significantly improves commit accuracy by leveraging project context and offers greater customization through templates and shell integration.
```
2025-12-25 00:04:44 +05:45
sankalp f2ff3327c3 feat(devgen): add custom template rendering function
- New Features:
  - Introduced `render_custom_template()` function to render Jinja2 templates from strings.
  - Enables dynamic template rendering without requiring file-based templates.
```
2025-12-25 00:04:40 +05:45
sankalp 37e5d94373 feat(devgen): enhance sanitize_ai_commit_message to handle bolding and labels
- Improve header detection by stripping bolding and cleaning labels like "1. Title:", "### Message:", etc.
- Stop processing lines when encountering a sponsor section
```
2025-12-24 23:49:37 +05:45
sankalp 40c9b3ca57 refactor(devgen/templates/commit): update commit message template guidelines
- Simplify and clarify the structure of commit message generation guidelines.
- Replace "Format" with "Guidelines" for better readability.
- Remove redundant section numbering.
- Streamline emoji usage instruction into a single line.
- Clarify that the output should exclude labels like "Title:" or "Format:".

**Enhancements:**
- Improved template structure for better maintainability.
- More concise and direct instructions for users.
2025-12-24 23:49:34 +05:45
sankalp 4d309c313d 1. Title: feat(root): add TODO.md for project roadmap and tasks 2025-12-24 23:44:23 +05:45
sankalp ba747c727c feat(devgen): improve sanitize_ai_commit_message function
The `sanitize_ai_commit_message` function in `devgen/utils.py` has been enhanced for better readability and maintainability. The changes include minor formatting improvements and a more consistent regex pattern definition.

**Enhancements & Refinements:**
- Improved code formatting by removing trailing whitespace and adding a comma after `re.IGNORECASE` in the regex pattern for better readability.
- Enhanced comment clarity by fixing a minor typo in the regex examples section.
```
2025-12-24 23:34:35 +05:45
sankalp 3635887a85 feat(devgen/modules): enhance changelog and license generation with improved file handling
This update introduces improvements to the changelog generator's file handling logic, including proper prepending of new entries and automatic header management. It also refines the license generator's template directory structure and cleans up minor formatting issues in the commit generator.

**New Features**
- Added intelligent changelog prepending logic that preserves existing headers and maintains proper formatting when updating files
- Implemented automatic `# CHANGELOG` header insertion for new changelog files

**Enhancements & Refinements**
- Changed changelog version headers from `#` to `##` for better markdown hierarchy
- Updated license generator to use `templates/licenses` directory instead of `prompt_templates/licenses`
- Removed unnecessary whitespace and improved code formatting in commit generator
- Enhanced changelog file writing logic to handle both new and existing files more robustly
```
2025-12-24 23:34:32 +05:45
sankalp a7507255c7 feat(root): update devgen-cli to version 0.2.3
The dependency `devgen-cli` has been upgraded from version 0.2.2 to 0.2.3 in the project's lockfile.

- **Enhancements & Refinements**
  - Updated `devgen-cli` dependency to version 0.2.3 in `uv.lock`
```
2025-12-24 23:29:47 +05:45
sankalp d373f95388 refactor(devgen): enhance AI commit message sanitization
The `sanitize_ai_commit_message` function has been significantly improved to better handle AI-generated commit messages, including support for bolded headers, more robust header detection, and a fallback mechanism for non-conventional messages.

**Enhancements & Refinements:**
- Added comprehensive docstring explaining the function's purpose and behavior
- Improved regex pattern to match conventional commit headers with optional bolding (e.g., **feat: ...**)
- Added case-insensitive matching for commit types
- Implemented a state-based approach with `found_header` flag for clearer logic
- Added automatic removal of bold markers (**) from headers
- Introduced fallback behavior to return the first non-empty line if no conventional header is found
- Improved code structure with better variable naming and comments
- Added early return for empty input strings

**Bug Fixes:**
- Fixed potential issue where the function could return empty strings for non-conventional messages
- Resolved edge case handling for messages without proper headers
```
2025-12-24 23:29:45 +05:45
sankalp fe7da77ed6 chore(root): Bump version to 0.2.3 2025-12-14 07:58:09 +05:45
sankalp 407458149b fix: remove invalid debug argument from OpenAI and OpenRouter providers
The debug argument is used internally by devgen but is not accepted by the OpenAI API client. This change filters it out from kwargs before the API call to prevent TypeError.
2025-12-14 07:57:16 +05:45
15 changed files with 432 additions and 58 deletions

No files matched your search

+33 -2
View File
@@ -20,7 +20,7 @@
## ⚡ Why DevGen?
**🧠 AI Brain**
Semantic commit messages powered by Gemini, OpenAI, Claude, HuggingFace, and OpenRouter. It reads your diffs and understands your code.
Semantic, context-aware commit messages powered by Gemini, OpenAI, Claude, HuggingFace, and OpenRouter. It reads your diffs, identifies project manifests, and understands your code.
**🚀 Battle Tested**
Generates **Conventional Commits** and **Semantic Versioning** compliant changelogs that actually make sense.
@@ -47,6 +47,9 @@ uv tool install devgen-cli
# Standard pip install
pip install devgen-cli
# Enable Shell Completion (bash/zsh/fish)
devgen --install-completion
```
## 🚀 Quick Start
@@ -80,8 +83,21 @@ devgen commit run --dry-run
# Commit and push in one go
devgen commit run --push
# Review and edit AI messages before committing
devgen commit run --check
# Made a mistake? Undo the last AI commit and keep changes staged
devgen commit undo
```
### 🧠 Context Awareness
DevGen isn't just looking at the code changes; it's looking at the big picture. It automatically detects and analyzes:
- **Manifests**: `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, etc.
- **Lock Files**: `uv.lock`, `package-lock.json`, `poetry.lock`, etc.
This allows the AI to understand *exactly* which library versions were updated, leading to hyper-accurate dependency commit messages.
### 📝 Changelogs & Release Notes
Turn your git history into beautiful, readable documentation.
@@ -114,7 +130,22 @@ Your settings live in `~/.devgen.yaml`. You can tweak your AI provider, model, a
| :--------- | :----------------------------------------------------------- |
| `provider` | `gemini`, `openai`, `anthropic`, `huggingface`, `openrouter` |
| `model` | Specific model name (e.g., `gemini-2.5-flash`, `gpt-4o`) |
| `emoji` | Enable/disable gitmojis in commits (`true`/`false`) |
| `emoji` | Enable/disable gitmojis in commits (`true`/`false`) |
| `custom_template` | Custom Jinja2 template for commit messages. |
### 🎨 Custom Templates
You can define your own commit message structure in `~/.devgen.yaml`. Use `devgen config info` to see available variables like `{{ group_name }}`, `{{ diff_text }}`, and `{{ context }}`.
Example:
```yaml
custom_template: |
{{ group_name }}: {{ diff_text }}
---
Manifests: {{ context }}
```
> [!TIP]
> Use `devgen config info` to see a full list of available variables and a template tip!
## 🤝 Contributing
+30
View File
@@ -0,0 +1,30 @@
# 📋 Project Roadmap & TODOs
This document outlines future improvements, features, and technical debt for `devgen`.
## 🚀 Priority: Short-Term (Reliability & DX)
- [ ] **Full Test Suite**: Implement `pytest` for all modules, especially `commit_generator` and `utils`.
- [x] **Interactive Commit Review**: Add a `--check` flag to `devgen commit run` that allows users to approve/edit the AI message before committing.
- [ ] **Project-Level Config**: Support `.devgen.yaml` in the repository root that overrides global `~/.devgen.yaml`.
- [ ] **Improved Gitignore Detection**: Auto-detect project type (Python, Node, Go) and suggest relevant `.gitignore` templates during `devgen gitignore run`.
## 🤖 AI & Intelligence
- [x] **Custom Prompts**: Allow users to define their own Jinja2 templates for commits in the config file(~/.devgen.yaml).
- [x] **Context-Aware Commits**: Send the manifest file (`package.json`, `pyproject.toml`) to the AI to provide better context for dependency changes.
- [ ] **Token Usage Dashboard**: Add a `devgen config tokens` command to estimate or track AI token consumption.
- [ ] **More Providers**: Add support for **Mistral**, **Cohere**, and **Groq** for faster/cheaper generations.
## 🛠️ Developer Experience (DX)
- [ ] **Pre-commit Hook Integration**: Provide a command to easily install `devgen` as a pre-commit hook.
- [x] **Shell Completion**: Implement `typer` shell completion for `bash`, `zsh`, and `fish`.
- [x] **Commit Undo**: `devgen commit undo` to easily revert the last AI-generated commit and unstage files.
- [x] **Multi-Group Commits**: Better handling of repositories where changes are spread across many unrelated directories.
## 🏗️ Refactoring & Technical Debt
- [ ] **Code Split**: Move CLI logic out of `devgen/cli` into separate service classes to improve testability.
- [ ] **Consistent Logging**: Standardize log output across all modules.
- [ ] **API Documentation**: Use Sphinx or MkDocs to generate documentation for the core modules.
## 🌍 Ecosystem
- [ ] **GitHub Action**: Create an official `devgen` GitHub Action for automated changelog updates on PR merge.
- [ ] **VS Code Extension**: A simple extension to trigger `devgen commit` from the sidebar.
+35 -1
View File
@@ -1,5 +1,6 @@
from typing import Annotated
import questionary
import typer
from devgen.modules.commit_generator import run_commit_engine
@@ -50,12 +51,19 @@ def run_commit(
help="Force regeneration of commit messages.",
),
] = False,
check: Annotated[
bool,
typer.Option(
"--check",
help="Review/edit commit message before committing.",
),
] = False,
) -> None:
log_file = get_main_log_path()
logger = configure_logger("devgen.cli.commit", log_file, console=debug)
logger.info(f"Log file: {log_file}")
logger.info(
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild}"
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild}, check={check}"
)
run_commit_engine(
@@ -63,6 +71,7 @@ def run_commit(
push=push,
debug=debug,
force_rebuild=force_rebuild,
check=check,
logger=logger,
)
@@ -94,3 +103,28 @@ def validate() -> None:
typer.echo(f"- {f}")
else:
typer.secho("[i] No staged files.", fg=typer.colors.RED)
@app.command("undo")
def undo_commit() -> None:
"""Undoes the last commit but keeps changes staged."""
from devgen.utils import run_git_command
try:
# Check if there's at least one commit
run_git_command(["git", "rev-parse", "HEAD"])
except Exception:
typer.secho("No commits found to undo.", fg=typer.colors.RED)
return
if questionary.confirm(
"Are you sure you want to undo the last commit? (Changes will remain staged)",
default=False,
).ask():
try:
run_git_command(["git", "reset", "--soft", "HEAD~1"])
typer.secho(
"Last commit undone. Changes are still staged.", fg=typer.colors.GREEN
)
except Exception as e:
typer.secho(f"Failed to undo commit: {e}", fg=typer.colors.RED)
+27
View File
@@ -167,3 +167,30 @@ def set_config() -> None:
_save_config(config)
typer.secho("\nConfiguration saved.", fg=typer.colors.GREEN)
typer.echo(yaml.dump(new_config, default_flow_style=False))
@app.command("info")
def config_info() -> None:
"""Show information about configuration options and templates."""
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Custom Template Variables", box=None)
table.add_column("Variable", style="cyan")
table.add_column("Description", style="white")
table.add_row("{{ group_name }}", "The folder name being committed (or 'root').")
table.add_row("{{ diff_text }}", "The git diff of the changes.")
table.add_row("{{ context }}", "Project context (manifest files content).")
console.print(table)
console.print(
"\n[bold]Tip:[/bold] If you hardcode emojis in your template, the AI will likely include them regardless of the 'emoji' setting.",
style="yellow",
)
console.print("\n[bold]Example Template:[/bold]")
console.print(
"custom_template: |\n [type]: [desc]\n \n Diff: {{ diff_text }}\n",
style="dim",
)
+1 -1
View File
@@ -15,7 +15,7 @@ from devgen.cli.setup import app as setup_app
app = typer.Typer(
name="devgen",
help="devgen-py: AI-Powered Git Commit & Release Automation.",
add_completion=False,
add_completion=True,
no_args_is_help=True,
rich_markup_mode="markdown",
)
+10 -5
View File
@@ -120,7 +120,7 @@ class ChangelogGenerator:
) -> str:
"""Generates markdown changelog from parsed commits."""
date_str = datetime.now().strftime("%Y-%m-%d")
md = [f"# {version} ({date_str})\n"]
md = [f"## {version} ({date_str})\n"]
# Order: Breaking, Features, Fixes, Docs, Others
order = [
@@ -163,14 +163,19 @@ class ChangelogGenerator:
if output_file:
path = Path(output_file)
# If file exists, prepend? For now, just overwrite or append logic could be complex.
# Let's implement prepend logic if file exists.
if path.exists():
old_content = path.read_text(encoding="utf-8")
new_content = old_content + "\n\n" + md_content
# Prepend the new content, assume # CHANGELOG is at the top or needs to be
if old_content.strip().startswith("# CHANGELOG"):
lines = old_content.split("\n", 1)
header = lines[0]
rest = lines[1] if len(lines) > 1 else ""
new_content = f"{header}\n\n{md_content}\n{rest.lstrip()}"
else:
new_content = f"# CHANGELOG\n\n{md_content}\n\n{old_content}"
path.write_text(new_content, encoding="utf-8")
else:
path.write_text(md_content, encoding="utf-8")
path.write_text(f"# CHANGELOG\n\n{md_content}", encoding="utf-8")
self.logger.info(f"Changelog written to {output_file}")
print(f" Changelog updated: {output_file}")
+222 -21
View File
@@ -1,3 +1,5 @@
import sys
import questionary
import subprocess
from collections import defaultdict
from datetime import datetime
@@ -13,6 +15,7 @@ from devgen.utils import (
load_template_env,
run_git_command,
sanitize_ai_commit_message,
render_custom_template,
)
from rich.console import Console
from rich.panel import Panel
@@ -26,6 +29,19 @@ class CommitEngineError(Exception):
pass
# Token Optimization Constants
MAX_DIFF_SIZE = 8000 # Maximum characters for a single group diff
IGNORE_PATTERNS = [
"uv.lock",
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"composer.lock",
"Gemfile.lock",
"poetry.lock",
]
class CommitEngine:
"""
Engine for generating AI-powered commit messages.
@@ -38,6 +54,7 @@ class CommitEngine:
push: bool = False,
debug: bool = False,
force_rebuild: bool = False,
check: bool = False,
provider: str = "gemini",
model: str = "gemini-2.5-flash",
logger: Any | None = None,
@@ -47,6 +64,7 @@ class CommitEngine:
self.push = push
self.debug = debug
self.force_rebuild = force_rebuild
self.check = check
self.provider = provider
self.model = model
self.logger = logger or configure_logger(
@@ -72,6 +90,7 @@ class CommitEngine:
def detect_changes(self) -> List[str]:
"""Detects changed, deleted, or untracked files."""
try:
# Get modified, deleted and untracked files
out = run_git_command(
[
"git",
@@ -82,27 +101,93 @@ class CommitEngine:
"--exclude-standard",
]
)
return [f.strip() for f in out.split("\n") if f.strip()]
files = [f.strip() for f in out.split("\n") if f.strip()]
# Also get staged files (in case of a previous failed run)
staged_out = run_git_command(["git", "diff", "--name-only", "--cached"])
staged_files = [f.strip() for f in staged_out.split("\n") if f.strip()]
# Combine and deduplicate
all_files = list(set(files + staged_files))
return all_files
except subprocess.CalledProcessError as e:
msg = f"Git command failed: {' '.join(e.cmd)}\nError: {e.stderr.strip()}"
self.logger.error(msg)
raise CommitEngineError(msg) from e
def group_files(self, files: List[str]) -> Dict[str, List[str]]:
"""Groups files by their parent directory."""
"""Groups files by their parent directory with smart merging if limit is exceeded."""
max_groups = self.config.get("max_groups", 5)
# 1. Initial grouping by immediate parent
groups = defaultdict(list)
for f in files:
parent = str(Path(f).parent)
key = "root" if parent == "." else parent
groups[key].append(f)
return groups
if len(groups) <= max_groups:
return dict(groups)
self.logger.info(
f"Too many groups ({len(groups)}). Merging based on max_groups={max_groups}"
)
# 2. Iteratively merge the deepest group into its parent until we hit the limit
while len(groups) > max_groups:
# Find the deepest path among the current group keys
# Skip 'root' as it's the top level
potential_merges = [k for k in groups.keys() if k != "root"]
if not potential_merges:
# This could happen if only 'root' is left or if max_groups is very small
break
# Deepest path is the one with the most segments
deepest = max(potential_merges, key=lambda p: len(Path(p).parts))
# Find the parent of this deepest path
parent_path = str(Path(deepest).parent)
new_key = "root" if parent_path == "." else parent_path
# Merge files into the new key
self.logger.debug(f"Merging group '{deepest}' into '{new_key}'")
groups[new_key].extend(groups.pop(deepest))
return dict(groups)
def generate_diff(self, files: List[str]) -> str:
"""Generates diff for specific files."""
"""Generates diff for specific files, with truncation for token optimization."""
try:
return run_git_command(
["git", "--no-pager", "diff", "--staged", "--", *files]
)
# Filter out very large metadata files that don't need full diffs
summary_info = []
files_to_diff = []
for f in files:
if any(p in f for p in IGNORE_PATTERNS):
summary_info.append(f"[METADATA UPDATED] {f}")
else:
files_to_diff.append(f)
diff = ""
if files_to_diff:
diff = run_git_command(
["git", "--no-pager", "diff", "--staged", "--", *files_to_diff]
)
full_content = "\n".join(summary_info + [diff]).strip()
# Truncate if too large
if len(full_content) > MAX_DIFF_SIZE:
self.logger.info(
f"Truncating diff from {len(full_content)} to {MAX_DIFF_SIZE} chars"
)
half = MAX_DIFF_SIZE // 2
return (
full_content[:half]
+ "\n\n... [DIFF TRUNCATED FOR TOKEN OPTIMIZATION] ...\n\n"
+ full_content[-half:]
)
return full_content
except subprocess.CalledProcessError as e:
msg = f"Git command failed: {' '.join(e.cmd)}\nError: {e.stderr.strip()}"
self.logger.error(msg)
@@ -165,9 +250,52 @@ class CommitEngine:
f"Git command failed: {' '.join(e.cmd)}\nError: {e.stderr.strip()}"
)
self.logger.error(msg)
# Check for "no upstream branch" specifically to give a hint?
if "no upstream branch" in msg.lower():
self.console.print(
"[warning]No upstream branch. Skipping push.[/warning]"
)
return
raise CommitEngineError(msg) from e
self.console.print("[bold green]Push successful.[/bold green]")
def _get_manifest_context(self) -> str:
"""Finds and reads manifest files to provide context."""
manifests = [
"pyproject.toml",
"package.json",
"go.mod",
"Cargo.toml",
"Gemfile",
"requirements.txt",
"uv.lock",
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"composer.lock",
"Gemfile.lock",
"poetry.lock",
]
found = []
for m in manifests:
path = Path(m)
if path.exists():
try:
content = path.read_text(encoding="utf-8")
# Take only first 100 lines to avoid token bloat
lines = content.splitlines()
summary = "\n".join(lines[:100])
if len(lines) > 100:
summary += "\n... (truncated)"
found.append(f"File: {m}\n---\n{summary}\n---")
except Exception:
continue
if not found:
return ""
return "\n\n### Project Context (Manifests)\n" + "\n".join(found)
def generate_message(self, group: str, diff: str, cache: Dict[str, str]) -> str:
"""Generates a commit message using AI or cache."""
if not self.force_rebuild and group in cache:
@@ -181,9 +309,33 @@ class CommitEngine:
model = self.kwargs.get("model") or self.config.get("model") or self.model
api_key = self.kwargs.get("api_key") or self.config.get("api_key")
use_emoji = self.config.get("emoji", True)
custom_template = self.config.get("custom_template")
template = self.template_env.get_template("commit_message.j2")
prompt = template.render(group_name=group, diff_text=diff, use_emoji=use_emoji)
manifest_context = self._get_manifest_context()
if manifest_context:
self.logger.info("Including manifest context in prompt")
if custom_template:
self.logger.info("Using custom template from config")
prompt = render_custom_template(
custom_template,
group_name=group,
diff_text=diff,
use_emoji=use_emoji,
context=manifest_context,
)
else:
template = self.template_env.get_template("commit_message.j2")
prompt = template.render(
group_name=group,
diff_text=diff,
use_emoji=use_emoji,
context=manifest_context,
)
# Automatically append emoji instruction based on global setting
emoji_instr = "Use emojis (🚀, 🐛)." if use_emoji else "No emojis."
prompt = f"{prompt.strip()}\n\n- {emoji_instr}"
with self.console.status("[bold blue]Generating commit message...[/bold blue]"):
raw = generate_with_ai(
@@ -239,20 +391,66 @@ class CommitEngine:
return True
msg = self.generate_message(group, diff, cache)
if not msg:
self.logger.error(f"Empty message for {group}")
try:
if not msg:
self.logger.error(f"Empty message for {group}")
self._reset_group(files)
return False
if self.dry_run:
self._log_dry_run(group, msg)
self._reset_group(files)
else:
if self.check:
self.console.print(
Panel(
Markdown(msg),
title=f"Proposed Commit Message [group: {group}]",
border_style="cyan",
)
)
choice = questionary.select(
"How would you like to proceed?",
choices=[
"Confirm",
"Edit",
"Abort",
],
default="Confirm",
).ask()
if not choice or choice == "Abort":
self.logger.info(f"Commit aborted by user at group {group}")
self._reset_group(files)
raise KeyboardInterrupt("User aborted")
if choice == "Edit":
msg = questionary.text(
"Edit commit message:",
multiline=True,
default=msg,
).ask()
if not msg:
self.logger.info(
f"Empty edit, commit cancelled for {group}"
)
self._reset_group(files)
return True
self.commit_staged(msg)
return True
except Exception as e:
self.logger.error(f"Failed to process group {group}: {e}")
self._reset_group(files)
return False
if self.dry_run:
self._log_dry_run(group, msg)
try:
run_git_command(["git", "reset", "HEAD", "--", *files])
except subprocess.CalledProcessError:
pass
else:
self.commit_staged(msg)
return True
def _reset_group(self, files: List[str]):
"""Unstages files for a group."""
try:
run_git_command(["git", "reset", "HEAD", "--", *files])
except subprocess.CalledProcessError:
pass # Ignore reset errors
def execute(self):
"""Main execution method."""
@@ -301,6 +499,9 @@ def run_commit_engine(**kwargs):
try:
engine = CommitEngine(**kwargs)
engine.execute()
except KeyboardInterrupt:
logger.warning("Interrupted by user. Exiting...")
sys.exit(0)
except Exception as e:
logger.error(f"Commit engine failed: {e}", exc_info=True)
+1 -3
View File
@@ -8,9 +8,7 @@ class LicenseGenerator:
"""Generates license files from templates."""
def __init__(self):
self.templates_dir = (
Path(__file__).parent.parent / "prompt_templates" / "licenses"
)
self.templates_dir = Path(__file__).parent.parent / "templates" / "licenses"
def list_licenses(self) -> List[Dict[str, str]]:
"""Lists available license templates."""
+3
View File
@@ -39,6 +39,9 @@ class OpenaiProvider:
# Add error handling if the client fails to initialize
raise RuntimeError(f"Failed to initialize OpenAI client: {e}")
# Remove debug from kwargs if present
kwargs.pop("debug", None)
# 2. Use the modern API syntax: client.chat.completions.create
response = client.chat.completions.create(
model=model or self.DEFAULT_MODEL,
+3
View File
@@ -19,6 +19,9 @@ class OpenrouterProvider:
api_key=api_key,
)
# Remove debug from kwargs if present
kwargs.pop("debug", None)
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
+8 -12
View File
@@ -1,15 +1,11 @@
Analyze the following git diff for `{{ group_name }}` and generate a semantic commit message.
Analyze the following git diff for the directory `{{ group_name }}` and generate a detailed semantic release commit message4. Format the output as:
1. A summary title in semantic release format (type(scope): summary) — scope can be the directory or 'root'.
2. A short summary paragraph if needed.
3. Bullet points listing the main changes, grouped as 'New Features', 'Enhancements & Refinements', and 'Bug Fixes' if relevant. Use bold for feature/section names.
{% if use_emoji %}
4. Use appropriate emojis for the commit type and sections (e.g., 🚀 for features, 🐛 for bugs).
{% else %}
4. Do NOT use any emojis in the commit message.
{% endif %}
Output only the commit message, don't include explanations or extra text.
Here is the git diff for `{{ group_name }}`:
Guidelines:
- Title line: `<type>(<scope>): <summary>` (scope is `{{ group_name }}` or `root`).
- Optional body: A short summary of why.
- Details: Bullet points for `New Features`, `Enhancements`, `Bug Fixes`.
- Output ONLY the message itself, without any labels like "Title:" or "Format:".
Diff for `{{ group_name }}`:
{{ diff_text }}
{{ context }}
+56 -10
View File
@@ -31,25 +31,63 @@ def is_file_recent(file_path: Path | str, max_age_minutes: int = 120) -> bool:
def sanitize_ai_commit_message(raw_text: str) -> str:
"""
Cleans up the AI-generated commit message.
It looks for a conventional commit header and extracts everything from there.
It handles bolded headers and common AI prefixes.
"""
if not raw_text:
return ""
lines = raw_text.strip().split("\n")
cleaned_lines = []
in_block = False
# Regex for conventional commit header
# Regex for conventional commit header (including optional bolding)
# Examples:
# - feat(root): summary
# - **fix: bug fix**
# - chore(deps)!: breaking change
header_pattern = re.compile(
r"^(feat|fix|chore|refactor|docs|style|test|build|ci)(\(.*\))?!?: .*"
r"^(\*\*)?(feat|fix|chore|refactor|docs|style|test|build|ci)(\(.*\))?!?: .*",
re.IGNORECASE,
)
cleaned_lines = []
found_header = False
for line in lines:
stripped = line.strip()
if in_block:
if header_pattern.match(stripped) or "**Sponsor**" in line:
if not found_header:
# First, strip bolding if it wraps the whole line or parts of it
# This is simpler than handling it in the regex
candidate = stripped.replace("**", "").strip()
# Clean the line by removing labels like "1. Title:", "### Message:", etc.
clean_line = re.sub(
r"^(#+\s+)?(\d+\.?|\*|-)?\s*(Title|Commit Message|Message):\s*",
"",
candidate,
flags=re.IGNORECASE,
).strip()
if header_pattern.match(clean_line):
found_header = True
cleaned_lines.append(clean_line)
else:
# If we hit another header or a known separator, we stop
if "**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 ""
if cleaned_lines:
return "\n".join(cleaned_lines).strip()
# Fallback: if no conventional commit header found, just take the first non-empty line
# to avoid failing completely, but log a warning if possible.
for line in lines:
if line.strip():
return line.strip()
return ""
def parse_markdown_sections(
@@ -174,6 +212,13 @@ def load_template_env(sub_dir: str) -> Environment:
return Environment(loader=FileSystemLoader(template_dir))
def render_custom_template(template_str: str, **context) -> str:
"""Renders a Jinja2 template from a string."""
env = Environment()
template = env.from_string(template_str)
return template.render(**context)
def load_config() -> Dict[str, Any]:
config_path = Path.home() / ".devgen.yaml"
@@ -183,6 +228,7 @@ def load_config() -> Dict[str, Any]:
"model": "gemini-2.5-flash",
"api_key": "",
"emoji": True,
"max_groups": 5,
}
try:
with config_path.open("w", encoding="utf-8") as f:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "devgen-cli"
version = "0.2.2"
version = "0.2.4"
description = "A collection of developer tools"
readme = "README.md"
requires-python = ">=3.10"
+1 -1
View File
@@ -6,7 +6,7 @@ from setuptools import find_packages, setup
setup(
name="devgen-cli",
version="0.2.2",
version="0.2.4",
packages=find_packages(),
include_package_data=True,
install_requires=[
Generated
+1 -1
View File
@@ -189,7 +189,7 @@ wheels = [
[[package]]
name = "devgen-cli"
version = "0.2.2"
version = "0.2.4"
source = { editable = "." }
dependencies = [
{ name = "anthropic" },