# git/operations/commit.py import git import os import logging from ..status.status import GitStatusManager logger = logging.getLogger(__name__) def parse_git_status(status_output): """ Parse git status --porcelain output into a structured format. Returns dict with staged and unstaged changes, identifying status of each file. """ changes = {} for line in status_output: if not line: continue index_status = line[0] # First character: staged status worktree_status = line[1] # Second character: unstaged status file_path = line[3:] changes[file_path] = { 'staged': index_status != ' ', 'staged_status': index_status, 'unstaged_status': worktree_status } return changes def commit_changes(repo_path, files, message): """ Commit changes to git repository, optimizing staging operations. Only re-stages files if their current staging status is incorrect. Args: repo_path: Path to git repository files: List of files to commit, or None/empty for all staged changes message: Commit message Returns: tuple: (success: bool, message: str) """ try: repo = git.Repo(repo_path) # If no specific files provided, commit all staged changes if not files: commit = repo.index.commit(message) # Update remote status after commit status_manager = GitStatusManager.get_instance(repo_path) if status_manager: status_manager.update_remote_status() return True, "Successfully committed all staged changes." # Get current status of the repository status_output = repo.git.status('--porcelain').splitlines() status = parse_git_status(status_output) # Track files that need staging operations to_add = [] to_remove = [] already_staged = [] for file_path in files: if file_path in status: file_status = status[file_path] # File is already properly staged if file_status['staged']: if file_status['staged_status'] == 'D': already_staged.append(('deleted', file_path)) else: already_staged.append(('modified', file_path)) continue # File needs to be staged if file_status['unstaged_status'] == 'D': to_remove.append(file_path) else: to_add.append(file_path) else: logger.warning(f"File not found in git status: {file_path}") # Perform necessary staging operations if to_add: logger.debug(f"Staging modified files: {to_add}") repo.index.add(to_add) if to_remove: logger.debug(f"Staging deleted files: {to_remove}") repo.index.remove(to_remove, working_tree=True) # Commit the changes commit = repo.index.commit(message) # Update remote status after commit status_manager = GitStatusManager.get_instance(repo_path) if status_manager: status_manager.update_remote_status() # Build detailed success message staged_counts = { 'added/modified': len(to_add), 'deleted': len(to_remove), 'already_staged': len(already_staged) } message_parts = [] if staged_counts['added/modified']: message_parts.append( f"{staged_counts['added/modified']} files staged") if staged_counts['deleted']: message_parts.append( f"{staged_counts['deleted']} deletions staged") if staged_counts['already_staged']: message_parts.append( f"{staged_counts['already_staged']} files already staged") if message_parts: details = " and ".join(message_parts) return True, f"Successfully committed changes ({details})" else: return True, "Successfully committed changes (no files needed staging)" except git.exc.GitCommandError as e: logger.error(f"Git command error committing changes: {str(e)}", exc_info=True) return False, f"Error committing changes: {str(e)}" except Exception as e: logger.error(f"Error committing changes: {str(e)}", exc_info=True) return False, f"Error committing changes: {str(e)}"