improvement to speed of merge conflict resolution
This commit is contained in:
+77
-17
@@ -1178,6 +1178,26 @@ def _resolve_git_conflicts_with_ai(
|
|||||||
merge_base = merge_base_result.stdout.strip() if merge_base_result.returncode == 0 else None
|
merge_base = merge_base_result.stdout.strip() if merge_base_result.returncode == 0 else None
|
||||||
debug(MODULE, "Found merge-base commit", merge_base=merge_base[:12] if merge_base else None)
|
debug(MODULE, "Found merge-base commit", merge_base=merge_base[:12] if merge_base else None)
|
||||||
|
|
||||||
|
# FIX: Copy NEW files FIRST before resolving conflicts
|
||||||
|
# This ensures dependencies exist before files that import them are written
|
||||||
|
changed_files = _get_changed_files_from_branch(project_dir, base_branch, spec_branch)
|
||||||
|
new_files = [(f, s) for f, s in changed_files if s == "A" and f not in conflicting_files]
|
||||||
|
|
||||||
|
if new_files:
|
||||||
|
print(muted(f" Copying {len(new_files)} new file(s) first (dependencies)..."))
|
||||||
|
for file_path, status in new_files:
|
||||||
|
try:
|
||||||
|
content = _get_file_content_from_ref(project_dir, spec_branch, file_path)
|
||||||
|
if content is not None:
|
||||||
|
target_path = project_dir / file_path
|
||||||
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target_path.write_text(content, encoding="utf-8")
|
||||||
|
subprocess.run(["git", "add", file_path], cwd=project_dir, capture_output=True)
|
||||||
|
resolved_files.append(file_path)
|
||||||
|
debug(MODULE, f"Copied new file: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
debug_warning(MODULE, f"Could not copy new file {file_path}: {e}")
|
||||||
|
|
||||||
for file_path in conflicting_files:
|
for file_path in conflicting_files:
|
||||||
debug(MODULE, f"Processing conflicting file: {file_path}")
|
debug(MODULE, f"Processing conflicting file: {file_path}")
|
||||||
print(muted(f" Processing: {file_path}"))
|
print(muted(f" Processing: {file_path}"))
|
||||||
@@ -1270,12 +1290,15 @@ def _resolve_git_conflicts_with_ai(
|
|||||||
"remaining_conflicts": remaining_conflicts,
|
"remaining_conflicts": remaining_conflicts,
|
||||||
}
|
}
|
||||||
|
|
||||||
# All conflicts resolved - now merge non-conflicting files
|
# All conflicts resolved - now merge remaining non-conflicting files
|
||||||
|
# (New files were already copied at the start)
|
||||||
print(muted(" Merging remaining files..."))
|
print(muted(" Merging remaining files..."))
|
||||||
|
|
||||||
# Get list of all changed files in worktree that aren't conflicting
|
# Get list of modified/deleted files (new files already copied at start)
|
||||||
changed_files = _get_changed_files_from_branch(project_dir, base_branch, spec_branch)
|
non_conflicting = [
|
||||||
non_conflicting = [f for f in changed_files if f not in conflicting_files]
|
(f, s) for f, s in changed_files
|
||||||
|
if f not in conflicting_files and s != "A" # Skip new files, already copied
|
||||||
|
]
|
||||||
|
|
||||||
for file_path, status in non_conflicting:
|
for file_path, status in non_conflicting:
|
||||||
try:
|
try:
|
||||||
@@ -1755,21 +1778,26 @@ def _create_conflict_file_with_git(
|
|||||||
worktree_content: str,
|
worktree_content: str,
|
||||||
base_content: Optional[str],
|
base_content: Optional[str],
|
||||||
project_dir: Path,
|
project_dir: Path,
|
||||||
) -> Optional[str]:
|
) -> tuple[Optional[str], bool]:
|
||||||
"""
|
"""
|
||||||
Use git merge-file to create a file with conflict markers.
|
Use git merge-file to create a file with conflict markers.
|
||||||
|
|
||||||
This produces a file with standard git conflict markers that can be
|
This produces a file with standard git conflict markers that can be
|
||||||
parsed to extract only the conflicting regions.
|
parsed to extract only the conflicting regions.
|
||||||
|
|
||||||
Returns the merged content with conflict markers, or None if no conflicts.
|
Returns:
|
||||||
|
Tuple of (merged_content, has_conflicts):
|
||||||
|
- (content, True) if there are conflict markers
|
||||||
|
- (content, False) if git cleanly merged (USE THIS RESULT!)
|
||||||
|
- (None, False) if merge failed
|
||||||
"""
|
"""
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
if not base_content:
|
if not base_content:
|
||||||
# Without a base, we can't do proper 3-way merge with markers
|
# Without a base, we can't do proper 3-way merge with markers
|
||||||
return None
|
debug(MODULE, "git merge-file: no base content available")
|
||||||
|
return None, False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create temp files for git merge-file
|
# Create temp files for git merge-file
|
||||||
@@ -1796,12 +1824,25 @@ def _create_conflict_file_with_git(
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
debug(MODULE, "git merge-file result",
|
||||||
|
return_code=result.returncode,
|
||||||
|
output_length=len(result.stdout) if result.stdout else 0,
|
||||||
|
has_conflict_markers='<<<<<<' in result.stdout if result.stdout else False)
|
||||||
|
|
||||||
# Return code > 0 means conflicts exist
|
# Return code > 0 means conflicts exist
|
||||||
if result.returncode > 0 and '<<<<<<' in result.stdout:
|
if result.returncode > 0 and '<<<<<<' in result.stdout:
|
||||||
return result.stdout
|
debug(MODULE, "git merge-file: has conflicts")
|
||||||
|
return result.stdout, True
|
||||||
elif result.returncode == 0:
|
elif result.returncode == 0:
|
||||||
# Clean merge, no conflicts
|
# Clean merge, no conflicts - THIS IS STILL USEFUL!
|
||||||
return None
|
debug(MODULE, "git merge-file: clean merge (no conflicts)")
|
||||||
|
return result.stdout, False
|
||||||
|
else:
|
||||||
|
# Some error occurred
|
||||||
|
debug_warning(MODULE, "git merge-file: unexpected result",
|
||||||
|
return_code=result.returncode,
|
||||||
|
stderr=result.stderr[:200] if result.stderr else None)
|
||||||
|
return None, False
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Cleanup temp files
|
# Cleanup temp files
|
||||||
@@ -1812,7 +1853,7 @@ def _create_conflict_file_with_git(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_warning(MODULE, f"git merge-file failed: {e}")
|
debug_warning(MODULE, f"git merge-file failed: {e}")
|
||||||
|
|
||||||
return None
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
def _merge_file_with_ai(
|
def _merge_file_with_ai(
|
||||||
@@ -1895,15 +1936,34 @@ def _merge_file_with_ai(
|
|||||||
if project_dir:
|
if project_dir:
|
||||||
task_intent = _get_task_intent(project_dir, spec_name)
|
task_intent = _get_task_intent(project_dir, spec_name)
|
||||||
|
|
||||||
# OPTIMIZATION: Try conflict-region-only merge first
|
# OPTIMIZATION: Try git merge-file first
|
||||||
# This is MUCH faster than sending entire files to AI
|
# This can either:
|
||||||
|
# 1. Cleanly merge (no AI needed - FASTEST)
|
||||||
|
# 2. Produce conflict markers (only send conflict regions to AI - FAST)
|
||||||
|
# 3. Fail (fall back to full-file AI merge - SLOW)
|
||||||
if project_dir and base_content:
|
if project_dir and base_content:
|
||||||
conflict_file = _create_conflict_file_with_git(
|
merged_content, has_conflicts = _create_conflict_file_with_git(
|
||||||
main_content, worktree_content, base_content, project_dir
|
main_content, worktree_content, base_content, project_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
if conflict_file:
|
# Case 1: Git cleanly merged - no AI needed!
|
||||||
conflicts, _ = parse_conflict_markers(conflict_file)
|
if merged_content and not has_conflicts:
|
||||||
|
debug_success(MODULE, "Git merge-file cleanly merged (no AI needed)",
|
||||||
|
file_path=file_path)
|
||||||
|
print(success(f" ✓ Git auto-merged (no conflicts)"))
|
||||||
|
|
||||||
|
# Validate syntax before returning
|
||||||
|
is_valid, error_msg = _validate_merged_syntax(file_path, merged_content, project_dir)
|
||||||
|
if is_valid:
|
||||||
|
return merged_content
|
||||||
|
else:
|
||||||
|
debug_warning(MODULE, "Git auto-merge produced invalid syntax, falling back to AI",
|
||||||
|
error=error_msg)
|
||||||
|
print(muted(f" Git auto-merge had syntax issues, trying AI..."))
|
||||||
|
|
||||||
|
# Case 2: Has conflict markers - use conflict-only AI merge (FAST)
|
||||||
|
if merged_content and has_conflicts:
|
||||||
|
conflicts, _ = parse_conflict_markers(merged_content)
|
||||||
|
|
||||||
if conflicts:
|
if conflicts:
|
||||||
# Calculate how much smaller this approach is
|
# Calculate how much smaller this approach is
|
||||||
@@ -1945,7 +2005,7 @@ def _merge_file_with_ai(
|
|||||||
expected=len(conflicts))
|
expected=len(conflicts))
|
||||||
|
|
||||||
# Reassemble the file with resolved conflicts
|
# Reassemble the file with resolved conflicts
|
||||||
merged = reassemble_with_resolutions(conflict_file, conflicts, resolutions)
|
merged = reassemble_with_resolutions(merged_content, conflicts, resolutions)
|
||||||
|
|
||||||
# Validate syntax
|
# Validate syntax
|
||||||
if project_dir:
|
if project_dir:
|
||||||
|
|||||||
Reference in New Issue
Block a user