package git import ( "fmt" "os" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) // RebaseEngine handles stacked rebase operations with rerere support type RebaseEngine struct { repo *gogit.Repository backend *GitBackend rerere *RerereManager conflictResolver *ConflictResolver repoPath string } // NewRebaseEngine creates a new RebaseEngine instance func NewRebaseEngine(repo *gogit.Repository, onyxPath, repoPath string) *RebaseEngine { return &RebaseEngine{ repo: repo, backend: NewGitBackend(repo), rerere: NewRerereManager(repo, onyxPath, repoPath), conflictResolver: NewConflictResolver(repo, repoPath), repoPath: repoPath, } } // RebaseStackResult contains the result of a stack rebase operation type RebaseStackResult struct { Success bool RebasedCommits []string FailedCommit string ConflictingFiles []ConflictInfo Message string } // RebaseStack rebases a stack of commits onto a new base func (re *RebaseEngine) RebaseStack(stack []string, onto string) (*RebaseStackResult, error) { result := &RebaseStackResult{ Success: true, RebasedCommits: []string{}, } if len(stack) == 0 { result.Message = "No commits to rebase" return result, nil } // Validate onto commit exists _, err := re.backend.GetCommit(onto) if err != nil { return nil, fmt.Errorf("invalid onto commit %s: %w", onto, err) } currentBase := onto // Rebase each commit in the stack sequentially for i, commitSHA := range stack { // Get the commit object commit, err := re.backend.GetCommit(commitSHA) if err != nil { result.Success = false result.FailedCommit = commitSHA result.Message = fmt.Sprintf("Failed to get commit %s: %v", commitSHA, err) return result, fmt.Errorf("failed to get commit: %w", err) } // Rebase this commit onto the current base newCommitSHA, err := re.rebaseSingleCommit(commit, currentBase) if err != nil { // Check if it's a conflict error conflicts, detectErr := re.conflictResolver.DetectConflicts() if detectErr == nil && len(conflicts) > 0 { result.Success = false result.FailedCommit = commitSHA result.ConflictingFiles = conflicts result.Message = fmt.Sprintf("Conflicts detected while rebasing commit %d/%d (%s)", i+1, len(stack), commitSHA[:8]) return result, nil } result.Success = false result.FailedCommit = commitSHA result.Message = fmt.Sprintf("Failed to rebase commit %s: %v", commitSHA, err) return result, err } result.RebasedCommits = append(result.RebasedCommits, newCommitSHA) currentBase = newCommitSHA } result.Message = fmt.Sprintf("Successfully rebased %d commit(s)", len(stack)) return result, nil } // rebaseSingleCommit rebases a single commit onto a new parent func (re *RebaseEngine) rebaseSingleCommit(commit *object.Commit, newParent string) (string, error) { // Record conflicts before attempting rebase (for rerere) if err := re.rerere.RecordConflicts(); err != nil { fmt.Fprintf(os.Stderr, "Warning: failed to record conflicts: %v\n", err) } // Get the commit's tree tree, err := commit.Tree() if err != nil { return "", fmt.Errorf("failed to get commit tree: %w", err) } // Check if there are any changes between the trees // For simplicity, we'll create a new commit with the same tree content // In a more sophisticated implementation, we would perform a three-way merge // Try to apply rerere resolutions first if re.rerere.IsEnabled() { applied, err := re.rerere.ApplyResolutions() if err != nil { fmt.Fprintf(os.Stderr, "Warning: failed to apply rerere resolutions: %v\n", err) } else if applied > 0 { fmt.Printf("Applied %d rerere resolution(s)\n", applied) } } // Perform a simple rebase by creating a new commit with the same tree but new parent // This is a simplified implementation - a full implementation would handle merges newCommitSHA, err := re.backend.CreateCommit( tree.Hash.String(), newParent, commit.Message, commit.Author.Name, ) if err != nil { return "", fmt.Errorf("failed to create rebased commit: %w", err) } return newCommitSHA, nil } // RebaseCommit rebases a single commit onto a new parent (public API) func (re *RebaseEngine) RebaseCommit(commitSHA, newParent string) (string, error) { commit, err := re.backend.GetCommit(commitSHA) if err != nil { return "", fmt.Errorf("failed to get commit: %w", err) } return re.rebaseSingleCommit(commit, newParent) } // ContinueRebase continues a rebase after conflict resolution func (re *RebaseEngine) ContinueRebase(stack []string, fromIndex int, onto string) (*RebaseStackResult, error) { // Record the resolution for rerere if err := re.rerere.RecordResolution(); err != nil { fmt.Fprintf(os.Stderr, "Warning: failed to record resolution: %v\n", err) } // Check if conflicts are resolved hasConflicts, err := re.conflictResolver.HasConflicts() if err != nil { return nil, fmt.Errorf("failed to check for conflicts: %w", err) } if hasConflicts { return &RebaseStackResult{ Success: false, Message: "Conflicts still exist. Please resolve all conflicts before continuing.", }, nil } // Continue rebasing from the next commit remainingStack := stack[fromIndex:] return re.RebaseStack(remainingStack, onto) } // AbortRebase aborts a rebase operation and returns to the original state func (re *RebaseEngine) AbortRebase(originalHead string) error { // Update HEAD to original commit hash := plumbing.NewHash(originalHead) worktree, err := re.repo.Worktree() if err != nil { return fmt.Errorf("failed to get worktree: %w", err) } // Checkout the original HEAD err = worktree.Checkout(&gogit.CheckoutOptions{ Hash: hash, Force: true, }) if err != nil { return fmt.Errorf("failed to checkout original HEAD: %w", err) } return nil } // GetRerereManager returns the rerere manager func (re *RebaseEngine) GetRerereManager() *RerereManager { return re.rerere } // GetConflictResolver returns the conflict resolver func (re *RebaseEngine) GetConflictResolver() *ConflictResolver { return re.conflictResolver }