improve merge conflicts for lock files
This commit is contained in:
+41
-21
@@ -82,11 +82,13 @@ from workspace.git_utils import (
|
||||
get_changed_files_from_branch as _get_changed_files_from_branch,
|
||||
is_process_running as _is_process_running,
|
||||
is_binary_file as _is_binary_file,
|
||||
is_lock_file as _is_lock_file,
|
||||
validate_merged_syntax as _validate_merged_syntax,
|
||||
create_conflict_file_with_git as _create_conflict_file_with_git,
|
||||
MAX_FILE_LINES_FOR_AI,
|
||||
MAX_PARALLEL_AI_MERGES,
|
||||
BINARY_EXTENSIONS,
|
||||
LOCK_FILES,
|
||||
MERGE_LOCK_TIMEOUT,
|
||||
)
|
||||
|
||||
@@ -664,15 +666,22 @@ def _resolve_git_conflicts_with_ai(
|
||||
simple_merges.append((file_path, None)) # None = delete
|
||||
debug(MODULE, f" {file_path}: deleted (no AI needed)")
|
||||
else:
|
||||
# File exists in both - needs AI merge
|
||||
files_needing_ai_merge.append(ParallelMergeTask(
|
||||
file_path=file_path,
|
||||
main_content=main_content,
|
||||
worktree_content=worktree_content,
|
||||
base_content=base_content,
|
||||
spec_name=spec_name,
|
||||
))
|
||||
debug(MODULE, f" {file_path}: needs AI merge")
|
||||
# File exists in both - check if it's a lock file
|
||||
if _is_lock_file(file_path):
|
||||
# Lock files should never go through AI - just take worktree version
|
||||
# User can run package manager install to regenerate if needed
|
||||
simple_merges.append((file_path, worktree_content))
|
||||
debug(MODULE, f" {file_path}: lock file (taking worktree version)")
|
||||
else:
|
||||
# Regular file - needs AI merge
|
||||
files_needing_ai_merge.append(ParallelMergeTask(
|
||||
file_path=file_path,
|
||||
main_content=main_content,
|
||||
worktree_content=worktree_content,
|
||||
base_content=base_content,
|
||||
spec_name=spec_name,
|
||||
))
|
||||
debug(MODULE, f" {file_path}: needs AI merge")
|
||||
|
||||
except Exception as e:
|
||||
print(error(f" ✗ Failed to categorize {file_path}: {e}"))
|
||||
@@ -693,7 +702,11 @@ def _resolve_git_conflicts_with_ai(
|
||||
target_path.write_text(merged_content, encoding="utf-8")
|
||||
subprocess.run(["git", "add", file_path], cwd=project_dir, capture_output=True)
|
||||
resolved_files.append(file_path)
|
||||
print(success(f" ✓ {file_path} (new file)"))
|
||||
# Determine the type for display
|
||||
if _is_lock_file(file_path):
|
||||
print(success(f" ✓ {file_path} (lock file - took worktree version)"))
|
||||
else:
|
||||
print(success(f" ✓ {file_path} (new file)"))
|
||||
else:
|
||||
# Delete the file
|
||||
target_path = project_dir / file_path
|
||||
@@ -758,14 +771,8 @@ def _resolve_git_conflicts_with_ai(
|
||||
if remaining_conflicts:
|
||||
print(muted(f" Failed: {len(remaining_conflicts)}"))
|
||||
|
||||
if remaining_conflicts:
|
||||
return {
|
||||
"success": False,
|
||||
"resolved_files": resolved_files,
|
||||
"remaining_conflicts": remaining_conflicts,
|
||||
}
|
||||
|
||||
# All conflicts resolved - now merge remaining non-conflicting files
|
||||
# ALWAYS process non-conflicting files, even if some conflicts failed
|
||||
# This ensures we get as much of the build as possible
|
||||
# (New files were already copied at the start)
|
||||
print(muted(" Merging remaining files..."))
|
||||
|
||||
@@ -799,18 +806,31 @@ def _resolve_git_conflicts_with_ai(
|
||||
if resolved_files:
|
||||
_record_merge_completion(project_dir, spec_name, resolved_files)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
# Build result - partial success if some files failed but we got others
|
||||
result = {
|
||||
"success": len(remaining_conflicts) == 0,
|
||||
"resolved_files": resolved_files,
|
||||
"stats": {
|
||||
"files_merged": len(resolved_files),
|
||||
"conflicts_resolved": len(conflicting_files),
|
||||
"conflicts_resolved": len(conflicting_files) - len(remaining_conflicts),
|
||||
"ai_assisted": ai_merged_count,
|
||||
"auto_merged": auto_merged_count,
|
||||
"parallel_ai_merges": len(files_needing_ai_merge),
|
||||
},
|
||||
}
|
||||
|
||||
# Add remaining conflicts if any (for UI to show what needs manual attention)
|
||||
if remaining_conflicts:
|
||||
result["remaining_conflicts"] = remaining_conflicts
|
||||
result["partial_success"] = len(resolved_files) > 0
|
||||
print()
|
||||
print(warning(f" ⚠ {len(remaining_conflicts)} file(s) could not be auto-merged:"))
|
||||
for conflict in remaining_conflicts:
|
||||
print(muted(f" - {conflict['file']}: {conflict['reason']}"))
|
||||
print(muted(" These files may need manual review."))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _get_file_content_from_ref(project_dir: Path, ref: str, file_path: str) -> Optional[str]:
|
||||
"""Get file content from a git ref (branch, commit, etc.)."""
|
||||
|
||||
@@ -19,13 +19,14 @@ import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Import merge_existing_build from workspace.py (which coexists with this package)
|
||||
# Import merge functions from workspace.py (which coexists with this package)
|
||||
# We use importlib to explicitly load workspace.py since Python prefers the package
|
||||
_workspace_file = Path(__file__).parent.parent / "workspace.py"
|
||||
_spec = importlib.util.spec_from_file_location("workspace_module", _workspace_file)
|
||||
_workspace_module = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_workspace_module)
|
||||
merge_existing_build = _workspace_module.merge_existing_build
|
||||
_run_parallel_merges = _workspace_module._run_parallel_merges
|
||||
|
||||
# Models and Enums
|
||||
from .models import (
|
||||
@@ -46,11 +47,13 @@ from .git_utils import (
|
||||
get_changed_files_from_branch,
|
||||
is_process_running,
|
||||
is_binary_file,
|
||||
is_lock_file,
|
||||
validate_merged_syntax,
|
||||
create_conflict_file_with_git,
|
||||
# Export private names for backward compatibility
|
||||
_is_process_running,
|
||||
_is_binary_file,
|
||||
_is_lock_file,
|
||||
_validate_merged_syntax,
|
||||
_get_file_content_from_ref,
|
||||
_get_changed_files_from_branch,
|
||||
@@ -59,6 +62,7 @@ from .git_utils import (
|
||||
MAX_FILE_LINES_FOR_AI,
|
||||
MAX_PARALLEL_AI_MERGES,
|
||||
BINARY_EXTENSIONS,
|
||||
LOCK_FILES,
|
||||
MERGE_LOCK_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,22 @@ from typing import Optional
|
||||
MAX_FILE_LINES_FOR_AI = 5000 # Skip AI for files larger than this
|
||||
MAX_PARALLEL_AI_MERGES = 5 # Limit concurrent AI merge operations
|
||||
|
||||
# Lock files that should NEVER go through AI merge
|
||||
# These are auto-generated and should just take the worktree version
|
||||
# then regenerate via package manager install
|
||||
LOCK_FILES = {
|
||||
'package-lock.json',
|
||||
'pnpm-lock.yaml',
|
||||
'yarn.lock',
|
||||
'bun.lockb',
|
||||
'Pipfile.lock',
|
||||
'poetry.lock',
|
||||
'Cargo.lock',
|
||||
'Gemfile.lock',
|
||||
'composer.lock',
|
||||
'go.sum',
|
||||
}
|
||||
|
||||
BINARY_EXTENSIONS = {
|
||||
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.bmp', '.svg',
|
||||
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
||||
@@ -120,6 +136,16 @@ def is_binary_file(file_path: str) -> bool:
|
||||
return Path(file_path).suffix.lower() in BINARY_EXTENSIONS
|
||||
|
||||
|
||||
def is_lock_file(file_path: str) -> bool:
|
||||
"""
|
||||
Check if a file is a package manager lock file.
|
||||
|
||||
Lock files should never go through AI merge - they're auto-generated
|
||||
and should just take the worktree version, then regenerate via install.
|
||||
"""
|
||||
return Path(file_path).name in LOCK_FILES
|
||||
|
||||
|
||||
def validate_merged_syntax(file_path: str, content: str, project_dir: Path) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate the syntax of merged code.
|
||||
@@ -277,6 +303,7 @@ def create_conflict_file_with_git(
|
||||
# Export the _is_process_running function for backward compatibility
|
||||
_is_process_running = is_process_running
|
||||
_is_binary_file = is_binary_file
|
||||
_is_lock_file = is_lock_file
|
||||
_validate_merged_syntax = validate_merged_syntax
|
||||
_get_file_content_from_ref = get_file_content_from_ref
|
||||
_get_changed_files_from_branch = get_changed_files_from_branch
|
||||
|
||||
Reference in New Issue
Block a user