Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
31 changes: 26 additions & 5 deletions commitloom/cli/cli_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions commitloom/cli/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions commitloom/core/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
33 changes: 33 additions & 0 deletions tests/test_cli_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()