// 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; namespace Terra.Arbitrator.GUI { public class ArbitratorController { private List _changes = new(); public IReadOnlyList Changes => _changes; public string InfoMessage { get; private set; } public string ErrorMessage { get; private set; } public bool IsLoading { get; private set; } public string LoadingMessage { get; private set; } = ""; 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 void 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(changes => _changes = changes) .Catch(HandleOperationError) .Finally(FinishOperation); } public void Pull() { if (IsLoading) return; StartOperation("Analyzing for conflicts..."); GitService.AnalyzePullConflicts() .Then(analysisResult => { if (analysisResult.HasConflicts) { FinishOperation(); 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(); } // --- 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(); }); } } }