Files
onyx/internal/git/rerere.go

287 lines
7.4 KiB
Go

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
}