Files
profilarr/backend/app/git/status/merge_conflicts.py
santiagosayshey df16d7c52f fix: merge/delete conflicts (#151)
feat: enhance merge conflict handling with improved status messages and conditional rendering
feat: improved modify / delete conflict handling
- fix ours/theirs messages for deleted files
- fix handling of choice moniker
- fix parsing of local/incoming name when deleted
2025-02-26 18:11:35 +10:30

142 lines
5.3 KiB
Python

import os
import yaml
import logging
from git import GitCommandError
from .conflict_comparison import create_conflict_summary, UNRESOLVED, RESOLVED, MODIFY_DELETE
logger = logging.getLogger(__name__)
def get_version_data(repo, ref, file_path):
"""Get YAML data from a specific version of a file"""
try:
content = repo.git.show(f'{ref}:{file_path}')
return yaml.safe_load(content) if content else None
except GitCommandError:
return None
def process_modify_delete_conflict(repo, file_path, deleted_in_head):
"""Handle case where one side modified while other deleted"""
try:
# Check if conflict is resolved
status_output = repo.git.status('--porcelain', file_path)
file_exists = os.path.exists(os.path.join(repo.working_dir, file_path))
is_staged = status_output and status_output[0] in ['M', 'A']
# Determine status
if (file_exists and is_staged) or (not file_exists
and status_output.startswith('D ')):
status = RESOLVED
else:
status = MODIFY_DELETE
# For delete conflicts, we need to extract the name for display purposes
# This will be the name of the actual file before it was deleted
basename = os.path.basename(file_path)
filename = os.path.splitext(basename)[0] # Strip extension
# Get metadata from existing version to extract name if possible
if file_exists:
# File exists locally, read it
try:
with open(os.path.join(repo.working_dir, file_path), 'r') as f:
existing_data = yaml.safe_load(f.read())
except Exception as read_error:
logger.warning(f"Could not read existing file {file_path}: {str(read_error)}")
existing_data = {'name': filename}
else:
# File was deleted locally, try to get from merge head
try:
existing_data = get_version_data(repo, 'MERGE_HEAD', file_path)
except Exception as merge_error:
logger.warning(f"Could not get merge head for {file_path}: {str(merge_error)}")
existing_data = {'name': filename}
# Simplified placeholder data for deleted version
if deleted_in_head:
# File was deleted in HEAD (local) but exists in MERGE_HEAD (incoming)
local_data = None # This indicates deleted
try:
# Try to get name from incoming
incoming_data = existing_data if existing_data else {'name': filename}
except Exception:
incoming_data = {'name': filename}
else:
# File exists in HEAD (local) but deleted in MERGE_HEAD (incoming)
try:
local_data = existing_data if existing_data else {'name': filename}
except Exception:
local_data = {'name': filename}
incoming_data = None # This indicates deleted
return create_conflict_summary(file_path, local_data, incoming_data, status)
except Exception as e:
logger.error(
f"Failed to process modify/delete conflict for {file_path}: {str(e)}"
)
return None
def process_regular_conflict(repo, file_path):
"""Handle standard merge conflict between two versions"""
try:
# Get both versions
ours_data = get_version_data(repo, 'HEAD', file_path)
theirs_data = get_version_data(repo, 'MERGE_HEAD', file_path)
if not ours_data and not theirs_data:
return None
# Check if conflict is resolved
status_output = repo.git.status('--porcelain', file_path)
status = UNRESOLVED if status_output.startswith('UU') else RESOLVED
return create_conflict_summary(file_path, ours_data, theirs_data,
status)
except Exception as e:
logger.error(f"Failed to process conflict for {file_path}: {str(e)}")
return None
def get_merge_conflicts(repo):
"""Get all merge conflicts in the repository"""
try:
# Check if we're in a merge state
if not os.path.exists(os.path.join(repo.git_dir, 'MERGE_HEAD')):
return []
conflicts = []
status = repo.git.status('--porcelain', '-z').split('\0')
# Process each status entry
for item in status:
if not item or len(item) < 4:
continue
x, y = item[0], item[1]
file_path = item[3:]
# Handle modify/delete conflicts
if (x == 'D' and y == 'U') or (x == 'U'
and y == 'D') or (x == 'A'
and y == 'U'):
conflict = process_modify_delete_conflict(
repo, file_path, x == 'D')
if conflict:
conflicts.append(conflict)
# Handle regular conflicts
elif 'U' in (x, y) or (x == 'D' and y == 'D'):
conflict = process_regular_conflict(repo, file_path)
if conflict:
conflicts.append(conflict)
return conflicts
except Exception as e:
logger.error(f"Failed to get merge conflicts: {str(e)}")
return []