From 3ae91197adbc80a9a8d5ec99d1e6d9bd675e555f Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Sun, 27 Apr 2025 14:08:10 -0400 Subject: [PATCH 1/3] Update [ghstack-poisoned] --- codemcp/code_command.py | 120 +++++++++++++++++++++++++++++++++++- codemcp/tools/edit_file.py | 19 +++++- codemcp/tools/write_file.py | 16 ++++- e2e/test_run_command.py | 21 ++++++- 4 files changed, 171 insertions(+), 5 deletions(-) diff --git a/codemcp/code_command.py b/codemcp/code_command.py index 97e9a49a..0cb27e2e 100644 --- a/codemcp/code_command.py +++ b/codemcp/code_command.py @@ -3,7 +3,7 @@ import logging import os import subprocess -from typing import Any, Dict, List, Optional, cast +from typing import Any, Dict, List, Optional, Tuple, cast import tomli @@ -15,6 +15,7 @@ "get_command_from_config", "check_for_changes", "run_code_command", + "run_formatter_without_commit", ] @@ -215,3 +216,120 @@ async def run_code_command( error_msg = f"Error during {command_name}: {e}" logging.error(error_msg) return f"Error: {error_msg}" + + +async def run_formatter_without_commit(file_path: str) -> Tuple[bool, str]: + """Run the formatter on a specific file without performing pre/post commit operations. + + This function attempts to be flexible in working with different formatter configurations: + 1. If the formatter is configured to run on specific files (like black path/to/file.py) + 2. If the formatter is configured to run on all files in a directory + + Args: + file_path: Absolute path to the file to format + + Returns: + A tuple containing (success_status, message) + """ + try: + # Get the project directory (repository root) + project_dir = os.path.dirname(file_path) + try: + project_dir = await get_repository_root(project_dir) + except (subprocess.SubprocessError, OSError, ValueError) as e: + logging.debug(f"Not in a git repository: {e}") + # Fall back to the directory containing the file + pass + + # Get the format command from config + format_command = get_command_from_config(project_dir, "format") + if not format_command: + return False, "No format command configured in codemcp.toml" + + # Use relative path from project_dir for the formatting command + rel_path = os.path.relpath(file_path, project_dir) + + # First try running the formatter with the specific file path + # This works with tools like black, prettier, etc. that accept file paths + try: + specific_command = format_command.copy() + [rel_path] + + result = await run_command( + specific_command, + cwd=project_dir, + check=True, + capture_output=True, + text=True, + ) + + # If we get here, the formatter successfully ran on the specific file + truncated_stdout = truncate_output_content(result.stdout, prefer_end=True) + return True, f"File formatted successfully:\n{truncated_stdout}" + except subprocess.CalledProcessError as e: + # If the specific file approach failed, try running the formatter without arguments + # This might work for formatters that automatically detect files to format + logging.debug( + f"Formatter failed with specific file, trying without file path" + ) + try: + # Run the formatter without specific file arguments + # Some formatters automatically find and format files + result = await run_command( + format_command, + cwd=project_dir, + check=True, + capture_output=True, + text=True, + ) + + # If we get here, the formatter ran successfully + truncated_stdout = truncate_output_content( + result.stdout, prefer_end=True + ) + return True, f"File formatted successfully:\n{truncated_stdout}" + except subprocess.CalledProcessError: + # Both approaches failed, return error from the first attempt + # as it's more likely to be relevant to the specific file + truncated_stdout = truncate_output_content( + e.output if e.output else "", prefer_end=True + ) + truncated_stderr = truncate_output_content( + e.stderr if e.stderr else "", prefer_end=True + ) + + # Include both stdout and stderr in the error message + stdout_info = ( + f"STDOUT:\n{truncated_stdout}" + if truncated_stdout + else "STDOUT: " + ) + stderr_info = ( + f"STDERR:\n{truncated_stderr}" + if truncated_stderr + else "STDERR: " + ) + return False, f"Formatter failed: {stdout_info}\n{stderr_info}" + except subprocess.CalledProcessError as e: + # Truncate stdout and stderr if needed + truncated_stdout = truncate_output_content( + e.output if e.output else "", prefer_end=True + ) + truncated_stderr = truncate_output_content( + e.stderr if e.stderr else "", prefer_end=True + ) + + # Include both stdout and stderr in the error message + stdout_info = ( + f"STDOUT:\n{truncated_stdout}" if truncated_stdout else "STDOUT: " + ) + stderr_info = ( + f"STDERR:\n{truncated_stderr}" if truncated_stderr else "STDERR: " + ) + error_msg = f"Format command failed with exit code {e.returncode}:\n{stdout_info}\n{stderr_info}" + + logging.error(f"Format command failed with exit code {e.returncode}") + return False, f"Error: {error_msg}" + except Exception as e: + error_msg = f"Error during formatting: {e}" + logging.error(error_msg) + return False, f"Error: {error_msg}" diff --git a/codemcp/tools/edit_file.py b/codemcp/tools/edit_file.py index 454b74c0..081947a6 100644 --- a/codemcp/tools/edit_file.py +++ b/codemcp/tools/edit_file.py @@ -9,6 +9,7 @@ from difflib import SequenceMatcher from typing import Any, Dict, List, Tuple +from ..code_command import run_formatter_without_commit from ..common import get_edit_snippet from ..file_utils import ( async_open_text, @@ -773,6 +774,22 @@ async def edit_file_content( if read_file_timestamps is not None: read_file_timestamps[full_file_path] = os.stat(full_file_path).st_mtime + # Try to run the formatter on the file + format_message = "" + formatter_success, formatter_output = await run_formatter_without_commit( + full_file_path + ) + if formatter_success: + logger.info(f"Auto-formatted {full_file_path}") + if formatter_output.strip(): + format_message = "\nAuto-formatted the file" + else: + # Only log warning if there was actually a format command configured but it failed + if not "No format command configured" in formatter_output: + logger.warning( + f"Failed to auto-format {full_file_path}: {formatter_output}" + ) + # Generate a snippet of the edited file to show in the response snippet = get_edit_snippet(content, old_string, new_string) @@ -787,4 +804,4 @@ async def edit_file_content( else: git_message = f"\n\nFailed to commit changes to git: {message}" - return f"Successfully edited {full_file_path}\n\nHere's a snippet of the edited file:\n{snippet}{git_message}" + return f"Successfully edited {full_file_path}\n\nHere's a snippet of the edited file:\n{snippet}{format_message}{git_message}" diff --git a/codemcp/tools/write_file.py b/codemcp/tools/write_file.py index 5ac56fea..3efa8f45 100644 --- a/codemcp/tools/write_file.py +++ b/codemcp/tools/write_file.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 +import logging import os +from ..code_command import run_formatter_without_commit from ..file_utils import ( check_file_path_and_permissions, check_git_tracking_for_existing_file, @@ -61,6 +63,18 @@ async def write_file_content( # Write the content with UTF-8 encoding and proper line endings await write_text_content(file_path, content, "utf-8", line_endings) + # Try to run the formatter on the file + format_message = "" + formatter_success, formatter_output = await run_formatter_without_commit(file_path) + if formatter_success: + logging.info(f"Auto-formatted {file_path}") + if formatter_output.strip(): + format_message = f"\nAuto-formatted the file" + else: + # Only log warning if there was actually a format command configured but it failed + if not "No format command configured" in formatter_output: + logging.warning(f"Failed to auto-format {file_path}: {formatter_output}") + # Commit the changes git_message = "" success, message = await commit_changes(file_path, description, chat_id) @@ -69,4 +83,4 @@ async def write_file_content( else: git_message = f"\nFailed to commit changes to git: {message}" - return f"Successfully wrote to {file_path}{git_message}" + return f"Successfully wrote to {file_path}{format_message}{git_message}" diff --git a/e2e/test_run_command.py b/e2e/test_run_command.py index d563ff82..dfca1d1a 100644 --- a/e2e/test_run_command.py +++ b/e2e/test_run_command.py @@ -60,7 +60,16 @@ def test_run_command_with_args(project_dir): f.write("test content") result = subprocess.run( - [sys.executable, "-m", "codemcp", "run", "list", test_file, "--path", project_dir], + [ + sys.executable, + "-m", + "codemcp", + "run", + "list", + test_file, + "--path", + project_dir, + ], capture_output=True, text=True, check=True, @@ -72,7 +81,15 @@ def test_run_command_error_exit_code(project_dir): """Test that error exit codes from the command are propagated.""" # This should return a non-zero exit code process = subprocess.run( - [sys.executable, "-m", "codemcp", "run", "exit_with_error", "--path", project_dir], + [ + sys.executable, + "-m", + "codemcp", + "run", + "exit_with_error", + "--path", + project_dir, + ], capture_output=True, text=True, check=False, From c14eec916490ddd742607eca2ffd9ad01adce1ef Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Sun, 27 Apr 2025 14:41:30 -0400 Subject: [PATCH 2/3] Update [ghstack-poisoned] --- codemcp/code_command.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/codemcp/code_command.py b/codemcp/code_command.py index 5a027949..40a59f1d 100644 --- a/codemcp/code_command.py +++ b/codemcp/code_command.py @@ -231,12 +231,7 @@ async def run_formatter_without_commit(file_path: str) -> Tuple[bool, str]: Propagates any unexpected errors during formatting """ # Get the project directory (repository root) - project_dir = os.path.dirname(file_path) - try: - project_dir = await get_repository_root(project_dir) - except (subprocess.SubprocessError, OSError, ValueError): - # Fall back to the directory containing the file if not in a git repo - pass + project_dir = await get_repository_root(project_dir) # Get the format command from config - this is the only expected failure mode format_command = get_command_from_config(project_dir, "format") From 2d3005ab73cb5647c34a7950b58efbc55d39d533 Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Sun, 27 Apr 2025 15:04:00 -0400 Subject: [PATCH 3/3] Update [ghstack-poisoned] --- codemcp/code_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codemcp/code_command.py b/codemcp/code_command.py index 40a59f1d..6789bbb3 100644 --- a/codemcp/code_command.py +++ b/codemcp/code_command.py @@ -231,7 +231,7 @@ async def run_formatter_without_commit(file_path: str) -> Tuple[bool, str]: Propagates any unexpected errors during formatting """ # Get the project directory (repository root) - project_dir = await get_repository_root(project_dir) + project_dir = await get_repository_root(file_path) # Get the format command from config - this is the only expected failure mode format_command = get_command_from_config(project_dir, "format")