package git import ( "bufio" "fmt" "os" "path/filepath" "strings" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/format/index" ) // ConflictInfo represents information about a merge conflict type ConflictInfo struct { FilePath string OursHash string TheirsHash string BaseHash string HasConflict bool } // ConflictResolver handles conflict detection and resolution guidance type ConflictResolver struct { repo *gogit.Repository repoPath string } // NewConflictResolver creates a new ConflictResolver instance func NewConflictResolver(repo *gogit.Repository, repoPath string) *ConflictResolver { return &ConflictResolver{ repo: repo, repoPath: repoPath, } } // DetectConflicts checks for merge conflicts in the working tree func (cr *ConflictResolver) DetectConflicts() ([]ConflictInfo, error) { idx, err := cr.repo.Storer.Index() if err != nil { return nil, fmt.Errorf("failed to read index: %w", err) } conflicts := []ConflictInfo{} // Check for conflicts in the index for _, entry := range idx.Entries { // Stage > 0 indicates a conflict if entry.Stage != 0 { // Find all stages for this file conflict := cr.findConflictStages(idx, entry.Name) if conflict.HasConflict { conflicts = append(conflicts, conflict) } } } return conflicts, nil } // findConflictStages finds all conflict stages for a file func (cr *ConflictResolver) findConflictStages(idx *index.Index, path string) ConflictInfo { conflict := ConflictInfo{ FilePath: path, HasConflict: false, } for _, entry := range idx.Entries { if entry.Name == path { switch entry.Stage { case 1: // Base/common ancestor conflict.BaseHash = entry.Hash.String() conflict.HasConflict = true case 2: // Ours (current branch) conflict.OursHash = entry.Hash.String() conflict.HasConflict = true case 3: // Theirs (incoming branch) conflict.TheirsHash = entry.Hash.String() conflict.HasConflict = true } } } return conflict } // HasConflicts checks if there are any conflicts in the working tree func (cr *ConflictResolver) HasConflicts() (bool, error) { conflicts, err := cr.DetectConflicts() if err != nil { return false, err } return len(conflicts) > 0, nil } // PresentConflicts presents conflicts to the user with clear guidance func (cr *ConflictResolver) PresentConflicts(conflicts []ConflictInfo) string { if len(conflicts) == 0 { return "No conflicts detected." } var sb strings.Builder sb.WriteString(fmt.Sprintf("\n%s\n", strings.Repeat("=", 70))) sb.WriteString(fmt.Sprintf(" MERGE CONFLICTS DETECTED (%d file(s))\n", len(conflicts))) sb.WriteString(fmt.Sprintf("%s\n\n", strings.Repeat("=", 70))) for i, conflict := range conflicts { sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, conflict.FilePath)) sb.WriteString(fmt.Sprintf(" Base: %s\n", conflict.BaseHash[:8])) sb.WriteString(fmt.Sprintf(" Ours: %s\n", conflict.OursHash[:8])) sb.WriteString(fmt.Sprintf(" Theirs: %s\n", conflict.TheirsHash[:8])) sb.WriteString("\n") } sb.WriteString("To resolve conflicts:\n") sb.WriteString(" 1. Edit the conflicting files to resolve conflicts\n") sb.WriteString(" 2. Look for conflict markers: <<<<<<<, =======, >>>>>>>\n") sb.WriteString(" 3. Remove the conflict markers after resolving\n") sb.WriteString(" 4. Stage the resolved files: git add \n") sb.WriteString(" 5. Continue the rebase: git rebase --continue\n") sb.WriteString(fmt.Sprintf("%s\n", strings.Repeat("=", 70))) return sb.String() } // GetConflictMarkers reads a file and extracts conflict marker sections func (cr *ConflictResolver) GetConflictMarkers(filePath string) ([]ConflictMarker, error) { fullPath := filepath.Join(cr.repoPath, filePath) file, err := os.Open(fullPath) if err != nil { return nil, fmt.Errorf("failed to open file: %w", err) } defer file.Close() markers := []ConflictMarker{} scanner := bufio.NewScanner(file) lineNum := 0 var currentMarker *ConflictMarker for scanner.Scan() { lineNum++ line := scanner.Text() if strings.HasPrefix(line, "<<<<<<<") { // Start of conflict currentMarker = &ConflictMarker{ FilePath: filePath, StartLine: lineNum, } } else if strings.HasPrefix(line, "=======") && currentMarker != nil { currentMarker.SeparatorLine = lineNum } else if strings.HasPrefix(line, ">>>>>>>") && currentMarker != nil { currentMarker.EndLine = lineNum markers = append(markers, *currentMarker) currentMarker = nil } } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("error reading file: %w", err) } return markers, nil } // ConflictMarker represents a conflict marker section in a file type ConflictMarker struct { FilePath string StartLine int SeparatorLine int EndLine int } // IsFileConflicted checks if a specific file has conflict markers func (cr *ConflictResolver) IsFileConflicted(filePath string) (bool, error) { markers, err := cr.GetConflictMarkers(filePath) if err != nil { return false, err } return len(markers) > 0, nil }