Code MCP
by ezyang
- codemcp
#!/usr/bin/env python3
import logging
import os
import subprocess
from .common import normalize_file_path
from .shell import run_command
__all__ = [
"is_git_repository",
"commit_pending_changes",
"commit_changes",
]
def is_git_repository(path: str) -> bool:
"""Check if the path is within a Git repository.
Args:
path: The file path to check
Returns:
True if path is in a Git repository, False otherwise
"""
try:
# Get the directory containing the file or use the path itself if it's a directory
directory = os.path.dirname(path) if os.path.isfile(path) else path
# Get the absolute path to ensure consistency
directory = os.path.abspath(directory)
# Run git command to verify this is a git repository
run_command(
["git", "rev-parse", "--is-inside-work-tree"],
cwd=directory,
check=True,
capture_output=True,
text=True,
)
# Also get the repository root to use for all git operations
try:
run_command(
["git", "rev-parse", "--show-toplevel"],
cwd=directory,
check=True,
capture_output=True,
text=True,
).stdout.strip()
# Store the repository root in a global or class variable if needed
# This could be used to ensure all git operations use the same root
return True
except (subprocess.SubprocessError, OSError):
# If we can't get the repo root, it's not a proper git repository
return False
except (subprocess.SubprocessError, OSError):
return False
def commit_pending_changes(file_path: str) -> tuple[bool, str]:
"""Commit any pending changes in the repository, excluding the target file.
Args:
file_path: The path to the file to exclude from committing
Returns:
A tuple of (success, message)
"""
try:
# First, check if this is a git repository
if not is_git_repository(file_path):
return False, "File is not in a Git repository"
directory = os.path.dirname(file_path)
# Check if the file is tracked by git
file_status = run_command(
["git", "ls-files", "--error-unmatch", file_path],
cwd=directory,
capture_output=True,
text=True,
check=False,
)
file_is_tracked = file_status.returncode == 0
# If the file is not tracked, return an error
if not file_is_tracked:
return (
False,
"File is not tracked by git. Please add the file to git tracking first using 'git add <file>'",
)
# Check if working directory has uncommitted changes
status_result = run_command(
["git", "status", "--porcelain"],
cwd=directory,
capture_output=True,
check=True,
text=True,
)
# If there are uncommitted changes (besides our target file), commit them first
if status_result.stdout and file_is_tracked:
# Get list of changed files
changed_files = []
for line in status_result.stdout.splitlines():
# Skip empty lines
if not line.strip():
continue
# git status --porcelain output format: XY filename
# where X is status in staging area, Y is status in working tree
# There are at least 2 spaces before the filename
parts = line.strip().split(" ", 1)
if len(parts) > 1:
# Extract the filename, removing any leading spaces
filename = parts[1].lstrip()
full_path = normalize_file_path(os.path.join(directory, filename))
# Skip our target file
if full_path != normalize_file_path(file_path):
changed_files.append(filename)
if changed_files:
# Commit other changes first with a default message
run_command(
["git", "add", "."],
cwd=directory,
check=True,
capture_output=True,
text=True,
)
run_command(
["git", "commit", "-m", "Snapshot before codemcp change"],
cwd=directory,
check=True,
capture_output=True,
text=True,
)
return True, "Committed pending changes"
return True, "No pending changes to commit"
except Exception as e:
logging.warning(
f"Exception suppressed when committing pending changes: {e!s}",
exc_info=True,
)
return False, f"Error committing pending changes: {e!s}"
def commit_changes(path: str, description: str) -> tuple[bool, str]:
"""Commit changes to a file or directory in Git.
Args:
path: The path to the file or directory to commit
description: Commit message describing the change
Returns:
A tuple of (success, message)
"""
try:
# First, check if this is a git repository
if not is_git_repository(path):
return False, f"Path '{path}' is not in a Git repository"
# Get absolute paths for consistency
abs_path = os.path.abspath(path)
# Get the directory - if path is a file, use its directory, otherwise use the path itself
directory = os.path.dirname(abs_path) if os.path.isfile(abs_path) else abs_path
# Try to get the git repository root for more reliable operations
try:
repo_root = run_command(
["git", "rev-parse", "--show-toplevel"],
cwd=directory,
check=True,
capture_output=True,
text=True,
).stdout.strip()
# Use the repo root as the working directory for git commands
git_cwd = repo_root
except (subprocess.SubprocessError, OSError):
# Fall back to the directory if we can't get the repo root
git_cwd = directory
# If it's a file, check if it exists
if os.path.isfile(abs_path) and not os.path.exists(abs_path):
return False, f"File does not exist: {abs_path}"
# Add the path to git - could be a file or directory
try:
# If path is a directory, do git add .
add_command = (
["git", "add", "."]
if os.path.isdir(abs_path)
else ["git", "add", abs_path]
)
add_result = run_command(
add_command,
cwd=git_cwd,
capture_output=True,
text=True,
check=False,
)
except Exception as e:
return False, f"Failed to add to Git: {str(e)}"
if add_result.returncode != 0:
return False, f"Failed to add to Git: {add_result.stderr}"
# First check if there's already a commit in the repository
has_commits = False
rev_parse_result = run_command(
["git", "rev-parse", "--verify", "HEAD"],
cwd=git_cwd,
capture_output=True,
text=True,
check=False,
)
has_commits = rev_parse_result.returncode == 0
# Check if there are any staged changes to commit
if has_commits:
# Check if there are any changes to commit after git add
diff_result = run_command(
["git", "diff-index", "--cached", "--quiet", "HEAD"],
cwd=git_cwd,
capture_output=True,
text=True,
check=False,
)
# If diff-index returns 0, there are no changes to commit
if diff_result.returncode == 0:
return (
True,
"No changes to commit (changes already committed or no changes detected)",
)
# Commit the change
commit_result = run_command(
["git", "commit", "-m", description],
cwd=git_cwd,
capture_output=True,
text=True,
check=False,
)
if commit_result.returncode != 0:
return False, f"Failed to commit changes: {commit_result.stderr}"
return True, "Changes committed successfully"
except Exception as e:
logging.warning(
f"Exception suppressed when committing changes: {e!s}", exc_info=True
)
return False, f"Error committing changes: {e!s}"