// Copyright (c) 2025 TerraByte Inc. // // This script acts as the Controller for the ArbitratorWindow. It manages all // state and business logic, separating it from the UI rendering code in the window. using System; using System.Linq; using UnityEditor; using Terra.Arbitrator.Settings; using Terra.Arbitrator.Services; using Terra.Arbitrator.Promises; using System.Collections.Generic; using UnityEngine; namespace Terra.Arbitrator.GUI { public class ArbitratorController { // --- Private State --- private List _changes = new(); private string _infoMessage; private string _errorMessage; private bool _isLoading; private string _loadingMessage = ""; // --- Public Properties for the View --- public IReadOnlyList Changes => _changes; public string InfoMessage => _infoMessage; public string ErrorMessage => _errorMessage; public bool IsLoading => _isLoading; public string LoadingMessage => _loadingMessage; // --- Communication with the View --- private readonly Action _requestRepaint; private readonly Func _displayDialog; /// /// Initializes the controller. /// /// A callback to the window's Repaint() method. /// A callback to EditorUtility.DisplayDialog for user confirmations. public ArbitratorController(Action requestRepaint, Func displayDialog) { _requestRepaint = requestRepaint; _displayDialog = displayDialog; } // --- Public Methods (called by the View) --- public void OnEnable() { // This logic is moved from ArbitratorWindow.OnEnable if (SessionState.GetBool(BetterGitStatePersistence.ResetQueueKey, false)) { SessionState.EraseString(BetterGitStatePersistence.ResetQueueKey); _infoMessage = "Multi-file reset complete. Pulling again to confirm..."; Pull(); } else { Refresh(); } } public void Refresh() { StartOperation("Refreshing status..."); UnstageStep() .Then(CompareStep) .Then(FinalizeRefresh) .Catch(HandleOperationError) .Finally(FinishOperation); } public void Pull() { if (_isLoading) return; StartOperation("Analyzing for conflicts..."); GitService.AnalyzePullConflicts() .Then(analysisResult => { if (analysisResult.HasConflicts) { // This part is synchronous UI, so it stays. FinishOperation(); // Stop loading while modal is open ConflictResolutionWindow.ShowWindow(analysisResult.ConflictingFiles, filesToActOn => { if (filesToActOn == null) return; if (filesToActOn.Any()) ResetMultipleFiles(filesToActOn); else ForcePull(); }); } else { StartOperation("No conflicts found. Pulling changes..."); GitService.PerformSafePull() .Then(successMessage => { _infoMessage = successMessage; // Refresh will handle its own start/finish states Refresh(); }) .Catch(ex => { // If the pull fails, we need to handle the error and finish HandleOperationError(ex); FinishOperation(); }); } }) .Catch(ex => { HandleOperationError(ex); FinishOperation(); }); } public void CommitAndPush(string commitMessage) { var selectedFiles = _changes.Where(c => c.IsSelectedForCommit).ToList(); var username = BetterGitSettings.Username; var email = BetterGitSettings.Email; StartOperation("Staging, committing, and pushing files..."); GitService.CommitAndPush(selectedFiles, commitMessage, username, email) .Then(successMessage => { _infoMessage = successMessage; Refresh(); }) .Catch(ex => { HandleOperationError(ex); FinishOperation(); }); } public void ResetFile(GitChange change) { if (_isLoading) return; var userConfirmed = _displayDialog("Confirm Reset", $"Are you sure you want to revert all local changes to '{change.FilePath}'? This action cannot be undone."); if (!userConfirmed) return; StartOperation($"Resetting {change.FilePath}..."); GitService.ResetFileChanges(change) .Then(successMessage => { _infoMessage = successMessage; Refresh(); }) .Catch(ex => { HandleOperationError(ex); FinishOperation(); }); } public void DiffFile(GitChange change) { if (_isLoading) return; StartOperation($"Launching diff for {change.FilePath}..."); GitService.LaunchExternalDiff(change) .Catch(HandleOperationError) .Finally(Refresh); } public void ResolveConflict(GitChange change) { if (_isLoading) return; StartOperation($"Opening merge tool for {change.FilePath}..."); GitService.LaunchMergeTool(change) .Then(successMessage => { _infoMessage = successMessage; }) .Catch(HandleOperationError) .Finally(Refresh); } public void ResetAll() { var userConfirmed = _displayDialog( "Confirm Reset All", "Are you sure you want to discard ALL local changes (modified, added, and untracked files)?\n\nThis action cannot be undone."); if (!userConfirmed) return; StartOperation("Discarding all local changes..."); GitService.ResetAllChanges() .Then(successMessage => { _infoMessage = successMessage; }) .Catch(HandleOperationError) .Finally(Refresh); } public void SetAllSelection(bool selected) { if (_changes == null) return; foreach (var change in _changes) { if (change.Status != LibGit2Sharp.ChangeKind.Conflicted) { change.IsSelectedForCommit = selected; } } } // --- Private Methods --- private static IPromise UnstageStep() { return GitService.UnstageAllFilesIfSafe(); } private IPromise> CompareStep(bool wasUnstaged) { if (wasUnstaged) { _infoMessage = "Found and unstaged files for review."; } return GitService.CompareLocalToRemote(); } private void FinalizeRefresh(List changes) { _changes = changes; if (string.IsNullOrEmpty(_infoMessage) && (_changes == null || _changes.Count == 0)) { _infoMessage = "You are up-to-date! No local changes detected."; } } // --- Shared Helper Methods --- private void StartOperation(string loadingMessage) { _isLoading = true; _loadingMessage = loadingMessage; ClearMessages(); _changes = null; _requestRepaint?.Invoke(); } private void HandleOperationError(Exception ex) { _errorMessage = $"Operation Failed: {ex.Message}"; } private void FinishOperation() { _isLoading = false; _requestRepaint?.Invoke(); } private void ClearMessages() { _errorMessage = null; _infoMessage = null; } private void ResetMultipleFiles(List filePaths) { _infoMessage = $"Starting reset for {filePaths.Count} file(s)... This may trigger script compilation."; _requestRepaint?.Invoke(); SessionState.SetString("BetterGit.ResetQueue", string.Join(";", filePaths)); EditorApplication.delayCall += BetterGitStatePersistence.ContinueInterruptedReset; } private void ForcePull() { StartOperation("Attempting to pull and create conflicts..."); EditorApplication.LockReloadAssemblies(); GitService.ForcePull() .Then(_ => { _infoMessage = "Pull resulted in conflicts. Please resolve them below."; }) .Catch(HandleOperationError) .Finally(() => { EditorApplication.UnlockReloadAssemblies(); AssetDatabase.Refresh(); Refresh(); }); } } }