feat(cli): add per-file commit generation option
This commit is contained in:
4 files changed
+93
-7
No files matched your search
@@ -202,6 +202,7 @@ devtools commit generate [OPTIONS]
|
||||
- `--sign` Sign commits with GPG
|
||||
- `--temperature FLOAT` AI temperature (0.0-1.0)
|
||||
- `--emoji/--no-emoji` Include emoji prefixes (default: disabled)
|
||||
- `--smart-group/--per-file` Group multi-file changes into one commit (default: smart-group)
|
||||
|
||||
Commit messages follow the conventional format (emojis optional):
|
||||
|
||||
@@ -214,6 +215,21 @@ When emojis are enabled, these prefixes are used:
|
||||
- ✅ test: Adding tests
|
||||
- 🔧 chore: Maintenance
|
||||
|
||||
Per-file vs grouped commits:
|
||||
|
||||
- Use `--smart-group` (default) to generate a single message that covers all staged changes.
|
||||
- Use `--per-file` to generate a message per changed file. The CLI will create separate git commits, one per file, each with its own AI-generated message.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Single grouped message (default behavior)
|
||||
devtools commit generate -c
|
||||
|
||||
# Separate commits per file
|
||||
devtools commit generate --per-file -c
|
||||
```
|
||||
|
||||
#### Changelog Generation
|
||||
|
||||
```bash
|
||||
|
||||
@@ -48,6 +48,11 @@ def cli():
|
||||
default=False,
|
||||
help="Include emoji prefixes in commit messages",
|
||||
)
|
||||
@click.option(
|
||||
"--smart-group/--per-file",
|
||||
default=True,
|
||||
help="Smartly group multiple file changes into one commit (disable to commit per-file)",
|
||||
)
|
||||
def generate(
|
||||
files: tuple,
|
||||
repo: str,
|
||||
@@ -58,6 +63,7 @@ def generate(
|
||||
sign: bool,
|
||||
temperature: float,
|
||||
emoji: bool,
|
||||
smart_group: bool,
|
||||
):
|
||||
"""Generate commit messages for staged changes"""
|
||||
try:
|
||||
@@ -88,20 +94,51 @@ def generate(
|
||||
console.print("[yellow]No staged changes found.[/yellow]")
|
||||
return
|
||||
|
||||
# Generate commit message
|
||||
# Generate commit message(s)
|
||||
task = progress.add_task("Generating commit message...", total=None)
|
||||
commit_message = ai_service.generate_commit_message(
|
||||
staged_changes, temperature
|
||||
)
|
||||
messages_by_file = None
|
||||
if smart_group:
|
||||
commit_message = ai_service.generate_commit_message(
|
||||
staged_changes, temperature
|
||||
)
|
||||
else:
|
||||
diffs_map = git_service.get_staged_changes_map(
|
||||
list(files) if files else None
|
||||
)
|
||||
messages_by_file = ai_service.generate_batch_messages(
|
||||
diffs_map, temperature
|
||||
)
|
||||
commit_message = None
|
||||
progress.update(task, completed=True)
|
||||
|
||||
# Show preview and confirm
|
||||
console.print("\n[bold]Generated commit message:[/bold]")
|
||||
console.print(Panel(commit_message, title="Preview", border_style="blue"))
|
||||
console.print("\n[bold]Generated commit message(s):[/bold]")
|
||||
if messages_by_file:
|
||||
for fp, msg in messages_by_file.items():
|
||||
console.print(
|
||||
Panel(f"{fp}\n\n{msg}", title="Preview", border_style="blue")
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
Panel(commit_message, title="Preview", border_style="blue")
|
||||
)
|
||||
|
||||
if commit:
|
||||
task = progress.add_task("Committing changes...", total=None)
|
||||
git_service.commit_changes(commit_message, sign=sign)
|
||||
if messages_by_file:
|
||||
# Commit each file separately with its message
|
||||
# Ensure only that file is included in the commit by using pathspec
|
||||
# Assumes files are already staged; git commit -- <file> will include only that path
|
||||
for fp, msg in messages_by_file.items():
|
||||
ok, out, err, code = git_service.commit_paths_verbose(
|
||||
msg, [fp], sign=sign
|
||||
)
|
||||
if not ok:
|
||||
raise Exception(
|
||||
f"Failed to commit {fp}: {err or out or code}"
|
||||
)
|
||||
else:
|
||||
git_service.commit_changes(commit_message, sign=sign)
|
||||
progress.update(task, completed=True)
|
||||
|
||||
if push:
|
||||
|
||||
@@ -29,6 +29,22 @@ class CommitGenGitService(GitService):
|
||||
f"Changes in {file}:\n{diff}" for file, diff in diffs.items()
|
||||
)
|
||||
|
||||
def get_staged_changes_map(
|
||||
self, files: Optional[List[str]] = None
|
||||
) -> dict[str, str]:
|
||||
"""Get staged changes as a mapping of file -> diff.
|
||||
|
||||
Args:
|
||||
files: Optional list of files to get changes for
|
||||
|
||||
Returns:
|
||||
Dict mapping file path to its staged diff
|
||||
"""
|
||||
diffs = self.get_all_diffs(staged=True)
|
||||
if files:
|
||||
diffs = {file: diff for file, diff in diffs.items() if file in files}
|
||||
return diffs
|
||||
|
||||
def get_unstaged_changes(self) -> str:
|
||||
"""Get unstaged changes as a diff."""
|
||||
diffs = self.get_all_diffs(staged=False)
|
||||
|
||||
@@ -94,6 +94,23 @@ class GitService:
|
||||
stdout, stderr, returncode = self._run_git_command(args)
|
||||
return returncode == 0, stdout, stderr, returncode
|
||||
|
||||
def commit_paths_verbose(
|
||||
self, message: str, files: List[str], sign: bool = False
|
||||
) -> Tuple[bool, str, str, int]:
|
||||
"""Create a commit including only the specified files.
|
||||
|
||||
Returns:
|
||||
(ok, stdout, stderr, returncode)
|
||||
"""
|
||||
if not files:
|
||||
return False, "", "no files specified", 1
|
||||
args = ["commit"]
|
||||
if sign:
|
||||
args.append("-S")
|
||||
args.extend(["-m", message, "--"] + list(files))
|
||||
stdout, stderr, returncode = self._run_git_command(args)
|
||||
return returncode == 0, stdout, stderr, returncode
|
||||
|
||||
def get_repo_name(self) -> str:
|
||||
"""Get the repository name."""
|
||||
stdout, _, _ = self._run_git_command(["rev-parse", "--show-toplevel"])
|
||||
|
||||
Reference in new issue
Block a user