devgen-cli 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devgen_cli-0.2.3/devgen_cli.egg-info → devgen_cli-0.2.4}/PKG-INFO +34 -3
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/README.md +33 -2
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/cli/commit.py +35 -1
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/cli/config.py +27 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/cli/main.py +1 -1
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/modules/changelog_generator.py +10 -5
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/modules/commit_generator.py +222 -21
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/modules/license_generator.py +1 -3
- devgen_cli-0.2.4/devgen/templates/commit/commit_message.j2 +11 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/utils.py +56 -10
- {devgen_cli-0.2.3 → devgen_cli-0.2.4/devgen_cli.egg-info}/PKG-INFO +34 -3
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/pyproject.toml +1 -1
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/setup.py +1 -1
- devgen_cli-0.2.3/devgen/templates/commit/commit_message.j2 +0 -15
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/LICENSE +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/MANIFEST.in +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/__init__.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/ai.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/cli/__init__.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/cli/changelog.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/cli/gitignore.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/cli/license.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/cli/release.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/cli/setup.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/modules/__init__.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/modules/gitignore_generator.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/modules/release_note_generator.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/providers/__init__.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/providers/anthropic.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/providers/gemini.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/providers/huggingface.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/providers/openai.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/providers/openrouter.py +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/agpl-3.0.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/apache-2.0.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/bsd-2-clause.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/bsd-3-clause.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/bsl-1.0.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/cc0-1.0.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/epl-2.0.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/gpl-2.0.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/gpl-3.0.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/lgpl-2.1.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/mit.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/mpl-2.0.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen/templates/licenses/unlicense.json +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen_cli.egg-info/SOURCES.txt +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen_cli.egg-info/dependency_links.txt +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen_cli.egg-info/entry_points.txt +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen_cli.egg-info/requires.txt +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/devgen_cli.egg-info/top_level.txt +0 -0
- {devgen_cli-0.2.3 → devgen_cli-0.2.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devgen-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: A collection of developer tools
|
|
5
5
|
Home-page: https://github.com/S4NKALP/devgen
|
|
6
6
|
Author: Sankalp Tharu
|
|
@@ -57,7 +57,7 @@ Dynamic: requires-python
|
|
|
57
57
|
## ⚡ Why DevGen?
|
|
58
58
|
|
|
59
59
|
**🧠 AI Brain**
|
|
60
|
-
Semantic commit messages powered by Gemini, OpenAI, Claude, HuggingFace, and OpenRouter. It reads your diffs and understands your code.
|
|
60
|
+
Semantic, context-aware commit messages powered by Gemini, OpenAI, Claude, HuggingFace, and OpenRouter. It reads your diffs, identifies project manifests, and understands your code.
|
|
61
61
|
|
|
62
62
|
**🚀 Battle Tested**
|
|
63
63
|
Generates **Conventional Commits** and **Semantic Versioning** compliant changelogs that actually make sense.
|
|
@@ -84,6 +84,9 @@ uv tool install devgen-cli
|
|
|
84
84
|
|
|
85
85
|
# Standard pip install
|
|
86
86
|
pip install devgen-cli
|
|
87
|
+
|
|
88
|
+
# Enable Shell Completion (bash/zsh/fish)
|
|
89
|
+
devgen --install-completion
|
|
87
90
|
```
|
|
88
91
|
|
|
89
92
|
## 🚀 Quick Start
|
|
@@ -117,8 +120,21 @@ devgen commit run --dry-run
|
|
|
117
120
|
|
|
118
121
|
# Commit and push in one go
|
|
119
122
|
devgen commit run --push
|
|
123
|
+
|
|
124
|
+
# Review and edit AI messages before committing
|
|
125
|
+
devgen commit run --check
|
|
126
|
+
|
|
127
|
+
# Made a mistake? Undo the last AI commit and keep changes staged
|
|
128
|
+
devgen commit undo
|
|
120
129
|
```
|
|
121
130
|
|
|
131
|
+
### 🧠 Context Awareness
|
|
132
|
+
DevGen isn't just looking at the code changes; it's looking at the big picture. It automatically detects and analyzes:
|
|
133
|
+
- **Manifests**: `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, etc.
|
|
134
|
+
- **Lock Files**: `uv.lock`, `package-lock.json`, `poetry.lock`, etc.
|
|
135
|
+
|
|
136
|
+
This allows the AI to understand *exactly* which library versions were updated, leading to hyper-accurate dependency commit messages.
|
|
137
|
+
|
|
122
138
|
### 📝 Changelogs & Release Notes
|
|
123
139
|
|
|
124
140
|
Turn your git history into beautiful, readable documentation.
|
|
@@ -151,7 +167,22 @@ Your settings live in `~/.devgen.yaml`. You can tweak your AI provider, model, a
|
|
|
151
167
|
| :--------- | :----------------------------------------------------------- |
|
|
152
168
|
| `provider` | `gemini`, `openai`, `anthropic`, `huggingface`, `openrouter` |
|
|
153
169
|
| `model` | Specific model name (e.g., `gemini-2.5-flash`, `gpt-4o`) |
|
|
154
|
-
| `emoji`
|
|
170
|
+
| `emoji` | Enable/disable gitmojis in commits (`true`/`false`) |
|
|
171
|
+
| `custom_template` | Custom Jinja2 template for commit messages. |
|
|
172
|
+
|
|
173
|
+
### 🎨 Custom Templates
|
|
174
|
+
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 }}`.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
```yaml
|
|
178
|
+
custom_template: |
|
|
179
|
+
{{ group_name }}: {{ diff_text }}
|
|
180
|
+
---
|
|
181
|
+
Manifests: {{ context }}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> [!TIP]
|
|
185
|
+
> Use `devgen config info` to see a full list of available variables and a template tip!
|
|
155
186
|
|
|
156
187
|
## 🤝 Contributing
|
|
157
188
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
## ⚡ Why DevGen?
|
|
21
21
|
|
|
22
22
|
**🧠 AI Brain**
|
|
23
|
-
Semantic commit messages powered by Gemini, OpenAI, Claude, HuggingFace, and OpenRouter. It reads your diffs and understands your code.
|
|
23
|
+
Semantic, context-aware commit messages powered by Gemini, OpenAI, Claude, HuggingFace, and OpenRouter. It reads your diffs, identifies project manifests, and understands your code.
|
|
24
24
|
|
|
25
25
|
**🚀 Battle Tested**
|
|
26
26
|
Generates **Conventional Commits** and **Semantic Versioning** compliant changelogs that actually make sense.
|
|
@@ -47,6 +47,9 @@ uv tool install devgen-cli
|
|
|
47
47
|
|
|
48
48
|
# Standard pip install
|
|
49
49
|
pip install devgen-cli
|
|
50
|
+
|
|
51
|
+
# Enable Shell Completion (bash/zsh/fish)
|
|
52
|
+
devgen --install-completion
|
|
50
53
|
```
|
|
51
54
|
|
|
52
55
|
## 🚀 Quick Start
|
|
@@ -80,8 +83,21 @@ devgen commit run --dry-run
|
|
|
80
83
|
|
|
81
84
|
# Commit and push in one go
|
|
82
85
|
devgen commit run --push
|
|
86
|
+
|
|
87
|
+
# Review and edit AI messages before committing
|
|
88
|
+
devgen commit run --check
|
|
89
|
+
|
|
90
|
+
# Made a mistake? Undo the last AI commit and keep changes staged
|
|
91
|
+
devgen commit undo
|
|
83
92
|
```
|
|
84
93
|
|
|
94
|
+
### 🧠 Context Awareness
|
|
95
|
+
DevGen isn't just looking at the code changes; it's looking at the big picture. It automatically detects and analyzes:
|
|
96
|
+
- **Manifests**: `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, etc.
|
|
97
|
+
- **Lock Files**: `uv.lock`, `package-lock.json`, `poetry.lock`, etc.
|
|
98
|
+
|
|
99
|
+
This allows the AI to understand *exactly* which library versions were updated, leading to hyper-accurate dependency commit messages.
|
|
100
|
+
|
|
85
101
|
### 📝 Changelogs & Release Notes
|
|
86
102
|
|
|
87
103
|
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
|
|
|
114
130
|
| :--------- | :----------------------------------------------------------- |
|
|
115
131
|
| `provider` | `gemini`, `openai`, `anthropic`, `huggingface`, `openrouter` |
|
|
116
132
|
| `model` | Specific model name (e.g., `gemini-2.5-flash`, `gpt-4o`) |
|
|
117
|
-
| `emoji`
|
|
133
|
+
| `emoji` | Enable/disable gitmojis in commits (`true`/`false`) |
|
|
134
|
+
| `custom_template` | Custom Jinja2 template for commit messages. |
|
|
135
|
+
|
|
136
|
+
### 🎨 Custom Templates
|
|
137
|
+
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 }}`.
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
```yaml
|
|
141
|
+
custom_template: |
|
|
142
|
+
{{ group_name }}: {{ diff_text }}
|
|
143
|
+
---
|
|
144
|
+
Manifests: {{ context }}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
> [!TIP]
|
|
148
|
+
> Use `devgen config info` to see a full list of available variables and a template tip!
|
|
118
149
|
|
|
119
150
|
## 🤝 Contributing
|
|
120
151
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
2
|
|
|
3
|
+
import questionary
|
|
3
4
|
import typer
|
|
4
5
|
|
|
5
6
|
from devgen.modules.commit_generator import run_commit_engine
|
|
@@ -50,12 +51,19 @@ def run_commit(
|
|
|
50
51
|
help="Force regeneration of commit messages.",
|
|
51
52
|
),
|
|
52
53
|
] = False,
|
|
54
|
+
check: Annotated[
|
|
55
|
+
bool,
|
|
56
|
+
typer.Option(
|
|
57
|
+
"--check",
|
|
58
|
+
help="Review/edit commit message before committing.",
|
|
59
|
+
),
|
|
60
|
+
] = False,
|
|
53
61
|
) -> None:
|
|
54
62
|
log_file = get_main_log_path()
|
|
55
63
|
logger = configure_logger("devgen.cli.commit", log_file, console=debug)
|
|
56
64
|
logger.info(f"Log file: {log_file}")
|
|
57
65
|
logger.info(
|
|
58
|
-
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild}"
|
|
66
|
+
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild}, check={check}"
|
|
59
67
|
)
|
|
60
68
|
|
|
61
69
|
run_commit_engine(
|
|
@@ -63,6 +71,7 @@ def run_commit(
|
|
|
63
71
|
push=push,
|
|
64
72
|
debug=debug,
|
|
65
73
|
force_rebuild=force_rebuild,
|
|
74
|
+
check=check,
|
|
66
75
|
logger=logger,
|
|
67
76
|
)
|
|
68
77
|
|
|
@@ -94,3 +103,28 @@ def validate() -> None:
|
|
|
94
103
|
typer.echo(f"- {f}")
|
|
95
104
|
else:
|
|
96
105
|
typer.secho("[i] No staged files.", fg=typer.colors.RED)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("undo")
|
|
109
|
+
def undo_commit() -> None:
|
|
110
|
+
"""Undoes the last commit but keeps changes staged."""
|
|
111
|
+
from devgen.utils import run_git_command
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Check if there's at least one commit
|
|
115
|
+
run_git_command(["git", "rev-parse", "HEAD"])
|
|
116
|
+
except Exception:
|
|
117
|
+
typer.secho("No commits found to undo.", fg=typer.colors.RED)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if questionary.confirm(
|
|
121
|
+
"Are you sure you want to undo the last commit? (Changes will remain staged)",
|
|
122
|
+
default=False,
|
|
123
|
+
).ask():
|
|
124
|
+
try:
|
|
125
|
+
run_git_command(["git", "reset", "--soft", "HEAD~1"])
|
|
126
|
+
typer.secho(
|
|
127
|
+
"Last commit undone. Changes are still staged.", fg=typer.colors.GREEN
|
|
128
|
+
)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
typer.secho(f"Failed to undo commit: {e}", fg=typer.colors.RED)
|
|
@@ -167,3 +167,30 @@ def set_config() -> None:
|
|
|
167
167
|
_save_config(config)
|
|
168
168
|
typer.secho("\nConfiguration saved.", fg=typer.colors.GREEN)
|
|
169
169
|
typer.echo(yaml.dump(new_config, default_flow_style=False))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command("info")
|
|
173
|
+
def config_info() -> None:
|
|
174
|
+
"""Show information about configuration options and templates."""
|
|
175
|
+
from rich.console import Console
|
|
176
|
+
from rich.table import Table
|
|
177
|
+
|
|
178
|
+
console = Console()
|
|
179
|
+
table = Table(title="Custom Template Variables", box=None)
|
|
180
|
+
table.add_column("Variable", style="cyan")
|
|
181
|
+
table.add_column("Description", style="white")
|
|
182
|
+
|
|
183
|
+
table.add_row("{{ group_name }}", "The folder name being committed (or 'root').")
|
|
184
|
+
table.add_row("{{ diff_text }}", "The git diff of the changes.")
|
|
185
|
+
table.add_row("{{ context }}", "Project context (manifest files content).")
|
|
186
|
+
|
|
187
|
+
console.print(table)
|
|
188
|
+
console.print(
|
|
189
|
+
"\n[bold]Tip:[/bold] If you hardcode emojis in your template, the AI will likely include them regardless of the 'emoji' setting.",
|
|
190
|
+
style="yellow",
|
|
191
|
+
)
|
|
192
|
+
console.print("\n[bold]Example Template:[/bold]")
|
|
193
|
+
console.print(
|
|
194
|
+
"custom_template: |\n [type]: [desc]\n \n Diff: {{ diff_text }}\n",
|
|
195
|
+
style="dim",
|
|
196
|
+
)
|
|
@@ -15,7 +15,7 @@ from devgen.cli.setup import app as setup_app
|
|
|
15
15
|
app = typer.Typer(
|
|
16
16
|
name="devgen",
|
|
17
17
|
help="devgen-py: AI-Powered Git Commit & Release Automation.",
|
|
18
|
-
add_completion=
|
|
18
|
+
add_completion=True,
|
|
19
19
|
no_args_is_help=True,
|
|
20
20
|
rich_markup_mode="markdown",
|
|
21
21
|
)
|
|
@@ -120,7 +120,7 @@ class ChangelogGenerator:
|
|
|
120
120
|
) -> str:
|
|
121
121
|
"""Generates markdown changelog from parsed commits."""
|
|
122
122
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
123
|
-
md = [f"
|
|
123
|
+
md = [f"## {version} ({date_str})\n"]
|
|
124
124
|
|
|
125
125
|
# Order: Breaking, Features, Fixes, Docs, Others
|
|
126
126
|
order = [
|
|
@@ -163,14 +163,19 @@ class ChangelogGenerator:
|
|
|
163
163
|
|
|
164
164
|
if output_file:
|
|
165
165
|
path = Path(output_file)
|
|
166
|
-
# If file exists, prepend? For now, just overwrite or append logic could be complex.
|
|
167
|
-
# Let's implement prepend logic if file exists.
|
|
168
166
|
if path.exists():
|
|
169
167
|
old_content = path.read_text(encoding="utf-8")
|
|
170
|
-
|
|
168
|
+
# Prepend the new content, assume # CHANGELOG is at the top or needs to be
|
|
169
|
+
if old_content.strip().startswith("# CHANGELOG"):
|
|
170
|
+
lines = old_content.split("\n", 1)
|
|
171
|
+
header = lines[0]
|
|
172
|
+
rest = lines[1] if len(lines) > 1 else ""
|
|
173
|
+
new_content = f"{header}\n\n{md_content}\n{rest.lstrip()}"
|
|
174
|
+
else:
|
|
175
|
+
new_content = f"# CHANGELOG\n\n{md_content}\n\n{old_content}"
|
|
171
176
|
path.write_text(new_content, encoding="utf-8")
|
|
172
177
|
else:
|
|
173
|
-
path.write_text(md_content, encoding="utf-8")
|
|
178
|
+
path.write_text(f"# CHANGELOG\n\n{md_content}", encoding="utf-8")
|
|
174
179
|
|
|
175
180
|
self.logger.info(f"Changelog written to {output_file}")
|
|
176
181
|
print(f" Changelog updated: {output_file}")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import questionary
|
|
1
3
|
import subprocess
|
|
2
4
|
from collections import defaultdict
|
|
3
5
|
from datetime import datetime
|
|
@@ -13,6 +15,7 @@ from devgen.utils import (
|
|
|
13
15
|
load_template_env,
|
|
14
16
|
run_git_command,
|
|
15
17
|
sanitize_ai_commit_message,
|
|
18
|
+
render_custom_template,
|
|
16
19
|
)
|
|
17
20
|
from rich.console import Console
|
|
18
21
|
from rich.panel import Panel
|
|
@@ -26,6 +29,19 @@ class CommitEngineError(Exception):
|
|
|
26
29
|
pass
|
|
27
30
|
|
|
28
31
|
|
|
32
|
+
# Token Optimization Constants
|
|
33
|
+
MAX_DIFF_SIZE = 8000 # Maximum characters for a single group diff
|
|
34
|
+
IGNORE_PATTERNS = [
|
|
35
|
+
"uv.lock",
|
|
36
|
+
"package-lock.json",
|
|
37
|
+
"pnpm-lock.yaml",
|
|
38
|
+
"yarn.lock",
|
|
39
|
+
"composer.lock",
|
|
40
|
+
"Gemfile.lock",
|
|
41
|
+
"poetry.lock",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
29
45
|
class CommitEngine:
|
|
30
46
|
"""
|
|
31
47
|
Engine for generating AI-powered commit messages.
|
|
@@ -38,6 +54,7 @@ class CommitEngine:
|
|
|
38
54
|
push: bool = False,
|
|
39
55
|
debug: bool = False,
|
|
40
56
|
force_rebuild: bool = False,
|
|
57
|
+
check: bool = False,
|
|
41
58
|
provider: str = "gemini",
|
|
42
59
|
model: str = "gemini-2.5-flash",
|
|
43
60
|
logger: Any | None = None,
|
|
@@ -47,6 +64,7 @@ class CommitEngine:
|
|
|
47
64
|
self.push = push
|
|
48
65
|
self.debug = debug
|
|
49
66
|
self.force_rebuild = force_rebuild
|
|
67
|
+
self.check = check
|
|
50
68
|
self.provider = provider
|
|
51
69
|
self.model = model
|
|
52
70
|
self.logger = logger or configure_logger(
|
|
@@ -72,6 +90,7 @@ class CommitEngine:
|
|
|
72
90
|
def detect_changes(self) -> List[str]:
|
|
73
91
|
"""Detects changed, deleted, or untracked files."""
|
|
74
92
|
try:
|
|
93
|
+
# Get modified, deleted and untracked files
|
|
75
94
|
out = run_git_command(
|
|
76
95
|
[
|
|
77
96
|
"git",
|
|
@@ -82,27 +101,93 @@ class CommitEngine:
|
|
|
82
101
|
"--exclude-standard",
|
|
83
102
|
]
|
|
84
103
|
)
|
|
85
|
-
|
|
104
|
+
files = [f.strip() for f in out.split("\n") if f.strip()]
|
|
105
|
+
|
|
106
|
+
# Also get staged files (in case of a previous failed run)
|
|
107
|
+
staged_out = run_git_command(["git", "diff", "--name-only", "--cached"])
|
|
108
|
+
staged_files = [f.strip() for f in staged_out.split("\n") if f.strip()]
|
|
109
|
+
|
|
110
|
+
# Combine and deduplicate
|
|
111
|
+
all_files = list(set(files + staged_files))
|
|
112
|
+
return all_files
|
|
86
113
|
except subprocess.CalledProcessError as e:
|
|
87
114
|
msg = f"Git command failed: {' '.join(e.cmd)}\nError: {e.stderr.strip()}"
|
|
88
115
|
self.logger.error(msg)
|
|
89
116
|
raise CommitEngineError(msg) from e
|
|
90
117
|
|
|
91
118
|
def group_files(self, files: List[str]) -> Dict[str, List[str]]:
|
|
92
|
-
"""Groups files by their parent directory."""
|
|
119
|
+
"""Groups files by their parent directory with smart merging if limit is exceeded."""
|
|
120
|
+
max_groups = self.config.get("max_groups", 5)
|
|
121
|
+
|
|
122
|
+
# 1. Initial grouping by immediate parent
|
|
93
123
|
groups = defaultdict(list)
|
|
94
124
|
for f in files:
|
|
95
125
|
parent = str(Path(f).parent)
|
|
96
126
|
key = "root" if parent == "." else parent
|
|
97
127
|
groups[key].append(f)
|
|
98
|
-
|
|
128
|
+
|
|
129
|
+
if len(groups) <= max_groups:
|
|
130
|
+
return dict(groups)
|
|
131
|
+
|
|
132
|
+
self.logger.info(
|
|
133
|
+
f"Too many groups ({len(groups)}). Merging based on max_groups={max_groups}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# 2. Iteratively merge the deepest group into its parent until we hit the limit
|
|
137
|
+
while len(groups) > max_groups:
|
|
138
|
+
# Find the deepest path among the current group keys
|
|
139
|
+
# Skip 'root' as it's the top level
|
|
140
|
+
potential_merges = [k for k in groups.keys() if k != "root"]
|
|
141
|
+
if not potential_merges:
|
|
142
|
+
# This could happen if only 'root' is left or if max_groups is very small
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
# Deepest path is the one with the most segments
|
|
146
|
+
deepest = max(potential_merges, key=lambda p: len(Path(p).parts))
|
|
147
|
+
|
|
148
|
+
# Find the parent of this deepest path
|
|
149
|
+
parent_path = str(Path(deepest).parent)
|
|
150
|
+
new_key = "root" if parent_path == "." else parent_path
|
|
151
|
+
|
|
152
|
+
# Merge files into the new key
|
|
153
|
+
self.logger.debug(f"Merging group '{deepest}' into '{new_key}'")
|
|
154
|
+
groups[new_key].extend(groups.pop(deepest))
|
|
155
|
+
|
|
156
|
+
return dict(groups)
|
|
99
157
|
|
|
100
158
|
def generate_diff(self, files: List[str]) -> str:
|
|
101
|
-
"""Generates diff for specific files."""
|
|
159
|
+
"""Generates diff for specific files, with truncation for token optimization."""
|
|
102
160
|
try:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
161
|
+
# Filter out very large metadata files that don't need full diffs
|
|
162
|
+
summary_info = []
|
|
163
|
+
files_to_diff = []
|
|
164
|
+
for f in files:
|
|
165
|
+
if any(p in f for p in IGNORE_PATTERNS):
|
|
166
|
+
summary_info.append(f"[METADATA UPDATED] {f}")
|
|
167
|
+
else:
|
|
168
|
+
files_to_diff.append(f)
|
|
169
|
+
|
|
170
|
+
diff = ""
|
|
171
|
+
if files_to_diff:
|
|
172
|
+
diff = run_git_command(
|
|
173
|
+
["git", "--no-pager", "diff", "--staged", "--", *files_to_diff]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
full_content = "\n".join(summary_info + [diff]).strip()
|
|
177
|
+
|
|
178
|
+
# Truncate if too large
|
|
179
|
+
if len(full_content) > MAX_DIFF_SIZE:
|
|
180
|
+
self.logger.info(
|
|
181
|
+
f"Truncating diff from {len(full_content)} to {MAX_DIFF_SIZE} chars"
|
|
182
|
+
)
|
|
183
|
+
half = MAX_DIFF_SIZE // 2
|
|
184
|
+
return (
|
|
185
|
+
full_content[:half]
|
|
186
|
+
+ "\n\n... [DIFF TRUNCATED FOR TOKEN OPTIMIZATION] ...\n\n"
|
|
187
|
+
+ full_content[-half:]
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return full_content
|
|
106
191
|
except subprocess.CalledProcessError as e:
|
|
107
192
|
msg = f"Git command failed: {' '.join(e.cmd)}\nError: {e.stderr.strip()}"
|
|
108
193
|
self.logger.error(msg)
|
|
@@ -165,9 +250,52 @@ class CommitEngine:
|
|
|
165
250
|
f"Git command failed: {' '.join(e.cmd)}\nError: {e.stderr.strip()}"
|
|
166
251
|
)
|
|
167
252
|
self.logger.error(msg)
|
|
253
|
+
# Check for "no upstream branch" specifically to give a hint?
|
|
254
|
+
if "no upstream branch" in msg.lower():
|
|
255
|
+
self.console.print(
|
|
256
|
+
"[warning]No upstream branch. Skipping push.[/warning]"
|
|
257
|
+
)
|
|
258
|
+
return
|
|
168
259
|
raise CommitEngineError(msg) from e
|
|
169
260
|
self.console.print("[bold green]Push successful.[/bold green]")
|
|
170
261
|
|
|
262
|
+
def _get_manifest_context(self) -> str:
|
|
263
|
+
"""Finds and reads manifest files to provide context."""
|
|
264
|
+
manifests = [
|
|
265
|
+
"pyproject.toml",
|
|
266
|
+
"package.json",
|
|
267
|
+
"go.mod",
|
|
268
|
+
"Cargo.toml",
|
|
269
|
+
"Gemfile",
|
|
270
|
+
"requirements.txt",
|
|
271
|
+
"uv.lock",
|
|
272
|
+
"package-lock.json",
|
|
273
|
+
"pnpm-lock.yaml",
|
|
274
|
+
"yarn.lock",
|
|
275
|
+
"composer.lock",
|
|
276
|
+
"Gemfile.lock",
|
|
277
|
+
"poetry.lock",
|
|
278
|
+
]
|
|
279
|
+
found = []
|
|
280
|
+
for m in manifests:
|
|
281
|
+
path = Path(m)
|
|
282
|
+
if path.exists():
|
|
283
|
+
try:
|
|
284
|
+
content = path.read_text(encoding="utf-8")
|
|
285
|
+
# Take only first 100 lines to avoid token bloat
|
|
286
|
+
lines = content.splitlines()
|
|
287
|
+
summary = "\n".join(lines[:100])
|
|
288
|
+
if len(lines) > 100:
|
|
289
|
+
summary += "\n... (truncated)"
|
|
290
|
+
found.append(f"File: {m}\n---\n{summary}\n---")
|
|
291
|
+
except Exception:
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
if not found:
|
|
295
|
+
return ""
|
|
296
|
+
|
|
297
|
+
return "\n\n### Project Context (Manifests)\n" + "\n".join(found)
|
|
298
|
+
|
|
171
299
|
def generate_message(self, group: str, diff: str, cache: Dict[str, str]) -> str:
|
|
172
300
|
"""Generates a commit message using AI or cache."""
|
|
173
301
|
if not self.force_rebuild and group in cache:
|
|
@@ -181,9 +309,33 @@ class CommitEngine:
|
|
|
181
309
|
model = self.kwargs.get("model") or self.config.get("model") or self.model
|
|
182
310
|
api_key = self.kwargs.get("api_key") or self.config.get("api_key")
|
|
183
311
|
use_emoji = self.config.get("emoji", True)
|
|
312
|
+
custom_template = self.config.get("custom_template")
|
|
313
|
+
|
|
314
|
+
manifest_context = self._get_manifest_context()
|
|
315
|
+
if manifest_context:
|
|
316
|
+
self.logger.info("Including manifest context in prompt")
|
|
317
|
+
|
|
318
|
+
if custom_template:
|
|
319
|
+
self.logger.info("Using custom template from config")
|
|
320
|
+
prompt = render_custom_template(
|
|
321
|
+
custom_template,
|
|
322
|
+
group_name=group,
|
|
323
|
+
diff_text=diff,
|
|
324
|
+
use_emoji=use_emoji,
|
|
325
|
+
context=manifest_context,
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
template = self.template_env.get_template("commit_message.j2")
|
|
329
|
+
prompt = template.render(
|
|
330
|
+
group_name=group,
|
|
331
|
+
diff_text=diff,
|
|
332
|
+
use_emoji=use_emoji,
|
|
333
|
+
context=manifest_context,
|
|
334
|
+
)
|
|
184
335
|
|
|
185
|
-
|
|
186
|
-
|
|
336
|
+
# Automatically append emoji instruction based on global setting
|
|
337
|
+
emoji_instr = "Use emojis (🚀, 🐛)." if use_emoji else "No emojis."
|
|
338
|
+
prompt = f"{prompt.strip()}\n\n- {emoji_instr}"
|
|
187
339
|
|
|
188
340
|
with self.console.status("[bold blue]Generating commit message...[/bold blue]"):
|
|
189
341
|
raw = generate_with_ai(
|
|
@@ -239,20 +391,66 @@ class CommitEngine:
|
|
|
239
391
|
return True
|
|
240
392
|
|
|
241
393
|
msg = self.generate_message(group, diff, cache)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
394
|
+
try:
|
|
395
|
+
if not msg:
|
|
396
|
+
self.logger.error(f"Empty message for {group}")
|
|
397
|
+
self._reset_group(files)
|
|
398
|
+
return False
|
|
245
399
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
400
|
+
if self.dry_run:
|
|
401
|
+
self._log_dry_run(group, msg)
|
|
402
|
+
self._reset_group(files)
|
|
403
|
+
else:
|
|
404
|
+
if self.check:
|
|
405
|
+
self.console.print(
|
|
406
|
+
Panel(
|
|
407
|
+
Markdown(msg),
|
|
408
|
+
title=f"Proposed Commit Message [group: {group}]",
|
|
409
|
+
border_style="cyan",
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
choice = questionary.select(
|
|
413
|
+
"How would you like to proceed?",
|
|
414
|
+
choices=[
|
|
415
|
+
"Confirm",
|
|
416
|
+
"Edit",
|
|
417
|
+
"Abort",
|
|
418
|
+
],
|
|
419
|
+
default="Confirm",
|
|
420
|
+
).ask()
|
|
421
|
+
|
|
422
|
+
if not choice or choice == "Abort":
|
|
423
|
+
self.logger.info(f"Commit aborted by user at group {group}")
|
|
424
|
+
self._reset_group(files)
|
|
425
|
+
raise KeyboardInterrupt("User aborted")
|
|
426
|
+
|
|
427
|
+
if choice == "Edit":
|
|
428
|
+
msg = questionary.text(
|
|
429
|
+
"Edit commit message:",
|
|
430
|
+
multiline=True,
|
|
431
|
+
default=msg,
|
|
432
|
+
).ask()
|
|
433
|
+
if not msg:
|
|
434
|
+
self.logger.info(
|
|
435
|
+
f"Empty edit, commit cancelled for {group}"
|
|
436
|
+
)
|
|
437
|
+
self._reset_group(files)
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
self.commit_staged(msg)
|
|
254
441
|
|
|
255
|
-
|
|
442
|
+
return True
|
|
443
|
+
except Exception as e:
|
|
444
|
+
self.logger.error(f"Failed to process group {group}: {e}")
|
|
445
|
+
self._reset_group(files)
|
|
446
|
+
return False
|
|
447
|
+
|
|
448
|
+
def _reset_group(self, files: List[str]):
|
|
449
|
+
"""Unstages files for a group."""
|
|
450
|
+
try:
|
|
451
|
+
run_git_command(["git", "reset", "HEAD", "--", *files])
|
|
452
|
+
except subprocess.CalledProcessError:
|
|
453
|
+
pass # Ignore reset errors
|
|
256
454
|
|
|
257
455
|
def execute(self):
|
|
258
456
|
"""Main execution method."""
|
|
@@ -301,6 +499,9 @@ def run_commit_engine(**kwargs):
|
|
|
301
499
|
try:
|
|
302
500
|
engine = CommitEngine(**kwargs)
|
|
303
501
|
engine.execute()
|
|
502
|
+
except KeyboardInterrupt:
|
|
503
|
+
logger.warning("Interrupted by user. Exiting...")
|
|
504
|
+
sys.exit(0)
|
|
304
505
|
except Exception as e:
|
|
305
506
|
logger.error(f"Commit engine failed: {e}", exc_info=True)
|
|
306
507
|
|
|
@@ -8,9 +8,7 @@ class LicenseGenerator:
|
|
|
8
8
|
"""Generates license files from templates."""
|
|
9
9
|
|
|
10
10
|
def __init__(self):
|
|
11
|
-
self.templates_dir = (
|
|
12
|
-
Path(__file__).parent.parent / "prompt_templates" / "licenses"
|
|
13
|
-
)
|
|
11
|
+
self.templates_dir = Path(__file__).parent.parent / "templates" / "licenses"
|
|
14
12
|
|
|
15
13
|
def list_licenses(self) -> List[Dict[str, str]]:
|
|
16
14
|
"""Lists available license templates."""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Analyze the following git diff for `{{ group_name }}` and generate a semantic commit message.
|
|
2
|
+
|
|
3
|
+
Guidelines:
|
|
4
|
+
- Title line: `<type>(<scope>): <summary>` (scope is `{{ group_name }}` or `root`).
|
|
5
|
+
- Optional body: A short summary of why.
|
|
6
|
+
- Details: Bullet points for `New Features`, `Enhancements`, `Bug Fixes`.
|
|
7
|
+
- Output ONLY the message itself, without any labels like "Title:" or "Format:".
|
|
8
|
+
|
|
9
|
+
Diff for `{{ group_name }}`:
|
|
10
|
+
{{ diff_text }}
|
|
11
|
+
{{ context }}
|
|
@@ -31,25 +31,63 @@ def is_file_recent(file_path: Path | str, max_age_minutes: int = 120) -> bool:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def sanitize_ai_commit_message(raw_text: str) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Cleans up the AI-generated commit message.
|
|
36
|
+
It looks for a conventional commit header and extracts everything from there.
|
|
37
|
+
It handles bolded headers and common AI prefixes.
|
|
38
|
+
"""
|
|
39
|
+
if not raw_text:
|
|
40
|
+
return ""
|
|
41
|
+
|
|
34
42
|
lines = raw_text.strip().split("\n")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
43
|
+
|
|
44
|
+
# Regex for conventional commit header (including optional bolding)
|
|
45
|
+
# Examples:
|
|
46
|
+
# - feat(root): summary
|
|
47
|
+
# - **fix: bug fix**
|
|
48
|
+
# - chore(deps)!: breaking change
|
|
38
49
|
header_pattern = re.compile(
|
|
39
|
-
r"^(feat|fix|chore|refactor|docs|style|test|build|ci)(\(.*\))?!?: .*"
|
|
50
|
+
r"^(\*\*)?(feat|fix|chore|refactor|docs|style|test|build|ci)(\(.*\))?!?: .*",
|
|
51
|
+
re.IGNORECASE,
|
|
40
52
|
)
|
|
41
53
|
|
|
54
|
+
cleaned_lines = []
|
|
55
|
+
found_header = False
|
|
56
|
+
|
|
42
57
|
for line in lines:
|
|
43
58
|
stripped = line.strip()
|
|
44
|
-
if
|
|
45
|
-
if
|
|
59
|
+
if not found_header:
|
|
60
|
+
# First, strip bolding if it wraps the whole line or parts of it
|
|
61
|
+
# This is simpler than handling it in the regex
|
|
62
|
+
candidate = stripped.replace("**", "").strip()
|
|
63
|
+
|
|
64
|
+
# Clean the line by removing labels like "1. Title:", "### Message:", etc.
|
|
65
|
+
clean_line = re.sub(
|
|
66
|
+
r"^(#+\s+)?(\d+\.?|\*|-)?\s*(Title|Commit Message|Message):\s*",
|
|
67
|
+
"",
|
|
68
|
+
candidate,
|
|
69
|
+
flags=re.IGNORECASE,
|
|
70
|
+
).strip()
|
|
71
|
+
|
|
72
|
+
if header_pattern.match(clean_line):
|
|
73
|
+
found_header = True
|
|
74
|
+
cleaned_lines.append(clean_line)
|
|
75
|
+
else:
|
|
76
|
+
# If we hit another header or a known separator, we stop
|
|
77
|
+
if "**Sponsor**" in line:
|
|
46
78
|
break
|
|
47
79
|
cleaned_lines.append(line)
|
|
48
|
-
elif header_pattern.match(stripped):
|
|
49
|
-
in_block = True
|
|
50
|
-
cleaned_lines.append(line)
|
|
51
80
|
|
|
52
|
-
|
|
81
|
+
if cleaned_lines:
|
|
82
|
+
return "\n".join(cleaned_lines).strip()
|
|
83
|
+
|
|
84
|
+
# Fallback: if no conventional commit header found, just take the first non-empty line
|
|
85
|
+
# to avoid failing completely, but log a warning if possible.
|
|
86
|
+
for line in lines:
|
|
87
|
+
if line.strip():
|
|
88
|
+
return line.strip()
|
|
89
|
+
|
|
90
|
+
return ""
|
|
53
91
|
|
|
54
92
|
|
|
55
93
|
def parse_markdown_sections(
|
|
@@ -174,6 +212,13 @@ def load_template_env(sub_dir: str) -> Environment:
|
|
|
174
212
|
return Environment(loader=FileSystemLoader(template_dir))
|
|
175
213
|
|
|
176
214
|
|
|
215
|
+
def render_custom_template(template_str: str, **context) -> str:
|
|
216
|
+
"""Renders a Jinja2 template from a string."""
|
|
217
|
+
env = Environment()
|
|
218
|
+
template = env.from_string(template_str)
|
|
219
|
+
return template.render(**context)
|
|
220
|
+
|
|
221
|
+
|
|
177
222
|
def load_config() -> Dict[str, Any]:
|
|
178
223
|
config_path = Path.home() / ".devgen.yaml"
|
|
179
224
|
|
|
@@ -183,6 +228,7 @@ def load_config() -> Dict[str, Any]:
|
|
|
183
228
|
"model": "gemini-2.5-flash",
|
|
184
229
|
"api_key": "",
|
|
185
230
|
"emoji": True,
|
|
231
|
+
"max_groups": 5,
|
|
186
232
|
}
|
|
187
233
|
try:
|
|
188
234
|
with config_path.open("w", encoding="utf-8") as f:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devgen-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: A collection of developer tools
|
|
5
5
|
Home-page: https://github.com/S4NKALP/devgen
|
|
6
6
|
Author: Sankalp Tharu
|
|
@@ -57,7 +57,7 @@ Dynamic: requires-python
|
|
|
57
57
|
## ⚡ Why DevGen?
|
|
58
58
|
|
|
59
59
|
**🧠 AI Brain**
|
|
60
|
-
Semantic commit messages powered by Gemini, OpenAI, Claude, HuggingFace, and OpenRouter. It reads your diffs and understands your code.
|
|
60
|
+
Semantic, context-aware commit messages powered by Gemini, OpenAI, Claude, HuggingFace, and OpenRouter. It reads your diffs, identifies project manifests, and understands your code.
|
|
61
61
|
|
|
62
62
|
**🚀 Battle Tested**
|
|
63
63
|
Generates **Conventional Commits** and **Semantic Versioning** compliant changelogs that actually make sense.
|
|
@@ -84,6 +84,9 @@ uv tool install devgen-cli
|
|
|
84
84
|
|
|
85
85
|
# Standard pip install
|
|
86
86
|
pip install devgen-cli
|
|
87
|
+
|
|
88
|
+
# Enable Shell Completion (bash/zsh/fish)
|
|
89
|
+
devgen --install-completion
|
|
87
90
|
```
|
|
88
91
|
|
|
89
92
|
## 🚀 Quick Start
|
|
@@ -117,8 +120,21 @@ devgen commit run --dry-run
|
|
|
117
120
|
|
|
118
121
|
# Commit and push in one go
|
|
119
122
|
devgen commit run --push
|
|
123
|
+
|
|
124
|
+
# Review and edit AI messages before committing
|
|
125
|
+
devgen commit run --check
|
|
126
|
+
|
|
127
|
+
# Made a mistake? Undo the last AI commit and keep changes staged
|
|
128
|
+
devgen commit undo
|
|
120
129
|
```
|
|
121
130
|
|
|
131
|
+
### 🧠 Context Awareness
|
|
132
|
+
DevGen isn't just looking at the code changes; it's looking at the big picture. It automatically detects and analyzes:
|
|
133
|
+
- **Manifests**: `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, etc.
|
|
134
|
+
- **Lock Files**: `uv.lock`, `package-lock.json`, `poetry.lock`, etc.
|
|
135
|
+
|
|
136
|
+
This allows the AI to understand *exactly* which library versions were updated, leading to hyper-accurate dependency commit messages.
|
|
137
|
+
|
|
122
138
|
### 📝 Changelogs & Release Notes
|
|
123
139
|
|
|
124
140
|
Turn your git history into beautiful, readable documentation.
|
|
@@ -151,7 +167,22 @@ Your settings live in `~/.devgen.yaml`. You can tweak your AI provider, model, a
|
|
|
151
167
|
| :--------- | :----------------------------------------------------------- |
|
|
152
168
|
| `provider` | `gemini`, `openai`, `anthropic`, `huggingface`, `openrouter` |
|
|
153
169
|
| `model` | Specific model name (e.g., `gemini-2.5-flash`, `gpt-4o`) |
|
|
154
|
-
| `emoji`
|
|
170
|
+
| `emoji` | Enable/disable gitmojis in commits (`true`/`false`) |
|
|
171
|
+
| `custom_template` | Custom Jinja2 template for commit messages. |
|
|
172
|
+
|
|
173
|
+
### 🎨 Custom Templates
|
|
174
|
+
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 }}`.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
```yaml
|
|
178
|
+
custom_template: |
|
|
179
|
+
{{ group_name }}: {{ diff_text }}
|
|
180
|
+
---
|
|
181
|
+
Manifests: {{ context }}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> [!TIP]
|
|
185
|
+
> Use `devgen config info` to see a full list of available variables and a template tip!
|
|
155
186
|
|
|
156
187
|
## 🤝 Contributing
|
|
157
188
|
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
Analyze the following git diff for the directory `{{ group_name }}` and generate a detailed semantic release commit message4. Format the output as:
|
|
3
|
-
1. A summary title in semantic release format (type(scope): summary) — scope can be the directory or 'root'.
|
|
4
|
-
2. A short summary paragraph if needed.
|
|
5
|
-
3. Bullet points listing the main changes, grouped as 'New Features', 'Enhancements & Refinements', and 'Bug Fixes' if relevant. Use bold for feature/section names.
|
|
6
|
-
{% if use_emoji %}
|
|
7
|
-
4. Use appropriate emojis for the commit type and sections (e.g., 🚀 for features, 🐛 for bugs).
|
|
8
|
-
{% else %}
|
|
9
|
-
4. Do NOT use any emojis in the commit message.
|
|
10
|
-
{% endif %}
|
|
11
|
-
Output only the commit message, don't include explanations or extra text.
|
|
12
|
-
|
|
13
|
-
Here is the git diff for `{{ group_name }}`:
|
|
14
|
-
|
|
15
|
-
{{ diff_text }}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|