package git import ( "crypto/sha1" "encoding/hex" "fmt" "io" "os" "path/filepath" "strings" gogit "github.com/go-git/go-git/v5" ) // RerereManager manages git rerere (reuse recorded resolution) functionality type RerereManager struct { repo *gogit.Repository cachePath string enabled bool repoPath string conflictResolver *ConflictResolver } // NewRerereManager creates a new RerereManager instance func NewRerereManager(repo *gogit.Repository, onyxPath, repoPath string) *RerereManager { cachePath := filepath.Join(onyxPath, "rerere_cache") return &RerereManager{ repo: repo, cachePath: cachePath, enabled: true, repoPath: repoPath, conflictResolver: NewConflictResolver(repo, repoPath), } } // Enable enables rerere functionality func (rm *RerereManager) Enable() { rm.enabled = true } // Disable disables rerere functionality func (rm *RerereManager) Disable() { rm.enabled = false } // IsEnabled returns whether rerere is enabled func (rm *RerereManager) IsEnabled() bool { return rm.enabled } // RecordConflicts records current conflicts for future resolution func (rm *RerereManager) RecordConflicts() error { if !rm.enabled { return nil } conflicts, err := rm.conflictResolver.DetectConflicts() if err != nil { return fmt.Errorf("failed to detect conflicts: %w", err) } if len(conflicts) == 0 { return nil } // For each conflict, create a cache entry for _, conflict := range conflicts { if err := rm.recordConflict(conflict); err != nil { // Log error but continue with other conflicts fmt.Fprintf(os.Stderr, "Warning: failed to record conflict for %s: %v\n", conflict.FilePath, err) } } return nil } // recordConflict records a single conflict func (rm *RerereManager) recordConflict(conflict ConflictInfo) error { // Read the conflicted file fullPath := filepath.Join(rm.repoPath, conflict.FilePath) content, err := os.ReadFile(fullPath) if err != nil { return fmt.Errorf("failed to read conflicted file: %w", err) } // Generate a unique ID for this conflict pattern conflictID := rm.generateConflictID(content) // Create cache directory for this conflict conflictDir := filepath.Join(rm.cachePath, conflictID) if err := os.MkdirAll(conflictDir, 0755); err != nil { return fmt.Errorf("failed to create conflict cache directory: %w", err) } // Save the preimage (conflict state) preimagePath := filepath.Join(conflictDir, "preimage") if err := os.WriteFile(preimagePath, content, 0644); err != nil { return fmt.Errorf("failed to write preimage: %w", err) } // Save metadata metadataPath := filepath.Join(conflictDir, "metadata") metadata := fmt.Sprintf("file=%s\nbase=%s\nours=%s\ntheirs=%s\n", conflict.FilePath, conflict.BaseHash, conflict.OursHash, conflict.TheirsHash) if err := os.WriteFile(metadataPath, []byte(metadata), 0644); err != nil { return fmt.Errorf("failed to write metadata: %w", err) } return nil } // RecordResolution records the resolution for previously recorded conflicts func (rm *RerereManager) RecordResolution() error { if !rm.enabled { return nil } // Find all recorded conflicts entries, err := os.ReadDir(rm.cachePath) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("failed to read rerere cache: %w", err) } for _, entry := range entries { if !entry.IsDir() { continue } conflictID := entry.Name() if err := rm.recordResolutionForConflict(conflictID); err != nil { // Log error but continue fmt.Fprintf(os.Stderr, "Warning: failed to record resolution for %s: %v\n", conflictID, err) } } return nil } // recordResolutionForConflict records the resolution for a specific conflict func (rm *RerereManager) recordResolutionForConflict(conflictID string) error { conflictDir := filepath.Join(rm.cachePath, conflictID) // Read metadata to get file path metadataPath := filepath.Join(conflictDir, "metadata") metadataContent, err := os.ReadFile(metadataPath) if err != nil { return fmt.Errorf("failed to read metadata: %w", err) } // Parse file path from metadata filePath := "" for _, line := range strings.Split(string(metadataContent), "\n") { if strings.HasPrefix(line, "file=") { filePath = strings.TrimPrefix(line, "file=") break } } if filePath == "" { return fmt.Errorf("file path not found in metadata") } // Check if file still has conflicts fullPath := filepath.Join(rm.repoPath, filePath) if _, err := os.Stat(fullPath); os.IsNotExist(err) { // File was deleted or doesn't exist, skip return nil } hasConflicts, err := rm.conflictResolver.IsFileConflicted(filePath) if err != nil { return fmt.Errorf("failed to check if file is conflicted: %w", err) } if hasConflicts { // Still has conflicts, not resolved yet return nil } // Read the resolved content resolvedContent, err := os.ReadFile(fullPath) if err != nil { return fmt.Errorf("failed to read resolved file: %w", err) } // Save the postimage (resolved state) postimagePath := filepath.Join(conflictDir, "postimage") if err := os.WriteFile(postimagePath, resolvedContent, 0644); err != nil { return fmt.Errorf("failed to write postimage: %w", err) } return nil } // ApplyResolutions applies previously recorded resolutions to current conflicts func (rm *RerereManager) ApplyResolutions() (int, error) { if !rm.enabled { return 0, nil } conflicts, err := rm.conflictResolver.DetectConflicts() if err != nil { return 0, fmt.Errorf("failed to detect conflicts: %w", err) } appliedCount := 0 for _, conflict := range conflicts { // Read the conflicted file fullPath := filepath.Join(rm.repoPath, conflict.FilePath) content, err := os.ReadFile(fullPath) if err != nil { continue } // Generate conflict ID conflictID := rm.generateConflictID(content) // Check if we have a resolution for this conflict postimagePath := filepath.Join(rm.cachePath, conflictID, "postimage") if _, err := os.Stat(postimagePath); os.IsNotExist(err) { continue } // Apply the resolution resolvedContent, err := os.ReadFile(postimagePath) if err != nil { continue } if err := os.WriteFile(fullPath, resolvedContent, 0644); err != nil { continue } appliedCount++ } return appliedCount, nil } // generateConflictID generates a unique ID for a conflict pattern func (rm *RerereManager) generateConflictID(content []byte) string { // Normalize conflict content by removing variable parts normalized := rm.normalizeConflict(content) // Generate SHA1 hash hash := sha1.New() io.WriteString(hash, normalized) return hex.EncodeToString(hash.Sum(nil)) } // normalizeConflict normalizes conflict content for matching func (rm *RerereManager) normalizeConflict(content []byte) string { // Convert to string str := string(content) // Remove commit hashes from conflict markers (they vary) lines := strings.Split(str, "\n") var normalized []string for _, line := range lines { if strings.HasPrefix(line, "<<<<<<<") { normalized = append(normalized, "<<<<<<<") } else if strings.HasPrefix(line, ">>>>>>>") { normalized = append(normalized, ">>>>>>>") } else { normalized = append(normalized, line) } } return strings.Join(normalized, "\n") } // ClearCache clears the rerere cache func (rm *RerereManager) ClearCache() error { return os.RemoveAll(rm.cachePath) } // GetCachePath returns the path to the rerere cache func (rm *RerereManager) GetCachePath() string { return rm.cachePath }