diff --git a/README.md b/README.md index 8786308..5820cb1 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ loom -y # Non-interactive mode - ๐Ÿค– **AI-Powered Analysis**: Intelligently analyzes your changes and generates structured, semantic commit messages - ๐Ÿงต **Smart Batching**: Weaves multiple changes into coherent, logical commits - ๐Ÿ“Š **Complexity Analysis**: Identifies when commits are getting too large or complex +- ๐ŸŒฟ **Branch Suggestions**: Offers to create a new branch for very large commits - ๐Ÿ’ฐ **Cost Control**: Built-in token and cost estimation to keep API usage efficient - ๐Ÿ“ˆ **Usage Metrics**: Track your usage, cost savings, and productivity gains with built-in metrics - ๐Ÿ” **Binary Support**: Special handling for binary files with size and type detection @@ -252,6 +253,7 @@ CommitLoom automatically: 2. Warns about potentially oversized commits 3. Suggests splitting changes when appropriate 4. Maintains context across split commits +5. Optionally creates a new branch when commits are very large ## ๐Ÿ› ๏ธ Development Status diff --git a/commitloom/cli/cli_handler.py b/commitloom/cli/cli_handler.py index dbb98f1..fdeb48e 100644 --- a/commitloom/cli/cli_handler.py +++ b/commitloom/cli/cli_handler.py @@ -5,10 +5,11 @@ import os import subprocess import sys +from datetime import datetime from dotenv import load_dotenv -from ..core.analyzer import CommitAnalyzer +from ..core.analyzer import CommitAnalysis, CommitAnalyzer from ..core.git import GitError, GitFile, GitOperations from ..services.ai_service import AIService from ..services.metrics import metrics_manager # noqa @@ -53,6 +54,18 @@ def __init__(self, test_mode: bool = False, api_key: str | None = None): self.combine_commits = False self.console = console + def _maybe_create_branch(self, analysis: CommitAnalysis) -> None: + """Offer to create a new branch if the commit is complex.""" + if not analysis.is_complex: + return + branch_name = f"loom-large-{datetime.now().strftime('%Y%m%d_%H%M%S')}" + if console.confirm_branch_creation(branch_name): + try: + self.git.create_and_checkout_branch(branch_name) + console.print_info(f"Switched to new branch {branch_name}") + except GitError as e: + console.print_error(str(e)) + def _process_single_commit(self, files: list[GitFile]) -> None: """Process files as a single commit.""" try: @@ -69,6 +82,8 @@ def _process_single_commit(self, files: list[GitFile]) -> None: # Print analysis console.print_warnings(analysis) + self._maybe_create_branch(analysis) + self._maybe_create_branch(analysis) try: # Generate commit message @@ -236,12 +251,18 @@ def _create_batches(self, changed_files: list[GitFile]) -> list[list[GitFile]]: console.print_warning("No valid files to process.") return [] - # Create batches from valid files + # Group files by top-level directory for smarter batching + grouped: dict[str, list[GitFile]] = {} + for f in valid_files: + parts = f.path.split(os.sep) + top_dir = parts[0] if len(parts) > 1 else "root" + grouped.setdefault(top_dir, []).append(f) + batches = [] batch_size = BATCH_THRESHOLD - for i in range(0, len(valid_files), batch_size): - batch = valid_files[i : i + batch_size] - batches.append(batch) + for group_files in grouped.values(): + for i in range(0, len(group_files), batch_size): + batches.append(group_files[i : i + batch_size]) return batches diff --git a/commitloom/cli/console.py b/commitloom/cli/console.py index bcda06b..6a59395 100644 --- a/commitloom/cli/console.py +++ b/commitloom/cli/console.py @@ -222,6 +222,17 @@ def confirm_batch_continue() -> bool: return False +def confirm_branch_creation(branch_name: str) -> bool: + """Ask user to confirm creation of a new branch for large commits.""" + if _auto_confirm: + return True + try: + prompt = f"Create a new branch '{branch_name}' for these large changes?" + return Confirm.ask(f"\n{prompt}") + except Exception: + return False + + def select_commit_strategy() -> str: """Ask user how they want to handle multiple commits.""" if _auto_confirm: diff --git a/commitloom/core/git.py b/commitloom/core/git.py index 3bfc11e..3c2b318 100644 --- a/commitloom/core/git.py +++ b/commitloom/core/git.py @@ -284,3 +284,18 @@ def unstage_file(file: str) -> None: except subprocess.CalledProcessError as e: error_msg = e.stderr if e.stderr else str(e) raise GitError(f"Failed to unstage file: {error_msg}") + + @staticmethod + def create_and_checkout_branch(branch: str) -> None: + """Create and switch to a new branch.""" + try: + result = subprocess.run( + ["git", "checkout", "-b", branch], + capture_output=True, + text=True, + check=True, + ) + GitOperations._handle_git_output(result, f"while creating branch {branch}") + except subprocess.CalledProcessError as e: + error_msg = e.stderr if e.stderr else str(e) + raise GitError(f"Failed to create branch '{branch}': {error_msg}") diff --git a/tests/test_cli_handler.py b/tests/test_cli_handler.py index 070af07..7d92108 100644 --- a/tests/test_cli_handler.py +++ b/tests/test_cli_handler.py @@ -6,6 +6,7 @@ import pytest from commitloom.cli.cli_handler import CommitLoom +from commitloom.core.analyzer import CommitAnalysis from commitloom.core.git import GitError, GitFile from commitloom.services.ai_service import TokenUsage @@ -250,3 +251,35 @@ def test_handle_batch_git_error(cli): result = cli._handle_batch(mock_files, 1, 1) assert result is None + + +def test_maybe_create_branch(cli): + """Ensure branch is created when commit is complex.""" + analysis = CommitAnalysis( + estimated_tokens=2000, + estimated_cost=0.2, + num_files=10, + warnings=[], + is_complex=True, + ) + cli.git.create_and_checkout_branch = MagicMock() + with patch("commitloom.cli.cli_handler.console") as mock_console: + mock_console.confirm_branch_creation.return_value = True + cli._maybe_create_branch(analysis) + cli.git.create_and_checkout_branch.assert_called_once() + + +def test_maybe_create_branch_not_complex(cli): + """Ensure no branch is created when commit is simple.""" + analysis = CommitAnalysis( + estimated_tokens=10, + estimated_cost=0.0, + num_files=1, + warnings=[], + is_complex=False, + ) + cli.git.create_and_checkout_branch = MagicMock() + with patch("commitloom.cli.cli_handler.console") as mock_console: + mock_console.confirm_branch_creation.return_value = True + cli._maybe_create_branch(analysis) + cli.git.create_and_checkout_branch.assert_not_called()