|
@@ -11,9 +11,14 @@ using Terra.Arbitrator.Settings;
|
|
|
using Terra.Arbitrator.Services;
|
|
|
using Terra.Arbitrator.Promises;
|
|
|
using System.Collections.Generic;
|
|
|
+using UnityEditor.SceneManagement;
|
|
|
|
|
|
namespace Terra.Arbitrator.GUI
|
|
|
{
|
|
|
+ public enum UserAction { Proceed, SaveAndProceed, Cancel }
|
|
|
+
|
|
|
+ public enum SortColumn { Commit, Status, FilePath }
|
|
|
+
|
|
|
public class ArbitratorController
|
|
|
{
|
|
|
private List<GitChange> _changes = new();
|
|
@@ -22,19 +27,25 @@ namespace Terra.Arbitrator.GUI
|
|
|
public string ErrorMessage { get; private set; }
|
|
|
public bool IsLoading { get; private set; }
|
|
|
public string LoadingMessage { get; private set; } = "";
|
|
|
+ public bool IsInConflictState { get; private set; }
|
|
|
+ public int CommitsToPull { get; private set; }
|
|
|
+ public SortColumn CurrentSortColumn { get; private set; } = SortColumn.FilePath;
|
|
|
|
|
|
private readonly Action _requestRepaint;
|
|
|
- private readonly Func<string, string, bool> _displayDialog;
|
|
|
+ private readonly Func<string, string, string, string, bool> _displayDialog;
|
|
|
+ private readonly Func<UserAction> _promptForUnsavedChanges;
|
|
|
|
|
|
/// <summary>
|
|
|
/// Initializes the controller.
|
|
|
/// </summary>
|
|
|
/// <param name="requestRepaint">A callback to the window's Repaint() method.</param>
|
|
|
/// <param name="displayDialog">A callback to EditorUtility.DisplayDialog for user confirmations.</param>
|
|
|
- public ArbitratorController(Action requestRepaint, Func<string, string, bool> displayDialog)
|
|
|
+ /// <param name="promptForUnsavedChanges">A callback to EditorUtility.DisplayDialog for user confirmations.</param>
|
|
|
+ public ArbitratorController(Action requestRepaint, Func<string, string, string, string, bool> displayDialog, Func<UserAction> promptForUnsavedChanges)
|
|
|
{
|
|
|
_requestRepaint = requestRepaint;
|
|
|
_displayDialog = displayDialog;
|
|
|
+ _promptForUnsavedChanges = promptForUnsavedChanges;
|
|
|
}
|
|
|
|
|
|
public void OnEnable()
|
|
@@ -54,10 +65,12 @@ namespace Terra.Arbitrator.GUI
|
|
|
public void Refresh()
|
|
|
{
|
|
|
StartOperation("Refreshing status...");
|
|
|
+ CommitsToPull = 0;
|
|
|
|
|
|
UnstageStep()
|
|
|
.Then(CompareStep)
|
|
|
- .Then(changes => _changes = changes)
|
|
|
+ .Then(FetchUpstreamStep)
|
|
|
+ .Then(FinalizeRefresh)
|
|
|
.Catch(HandleOperationError)
|
|
|
.Finally(FinishOperation);
|
|
|
}
|
|
@@ -65,42 +78,71 @@ namespace Terra.Arbitrator.GUI
|
|
|
public void Pull()
|
|
|
{
|
|
|
if (IsLoading) return;
|
|
|
+ if (CancelOperationIfUnsavedScenes()) return;
|
|
|
+ if (CommitsToPull > 0)
|
|
|
+ {
|
|
|
+ if (!_displayDialog("Confirm Pull", $"There are {CommitsToPull} incoming changes. Are you sure you want to pull?", "Yes, Pull", "Cancel"))
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
StartOperation("Analyzing for conflicts...");
|
|
|
|
|
|
GitService.AnalyzePullConflicts()
|
|
|
.Then(analysisResult =>
|
|
|
{
|
|
|
+ FinishOperation(); // Stop loading before showing dialog
|
|
|
if (analysisResult.HasConflicts)
|
|
|
{
|
|
|
- FinishOperation();
|
|
|
- ConflictResolutionWindow.ShowWindow(analysisResult.ConflictingFiles, filesToActOn =>
|
|
|
- {
|
|
|
- if (filesToActOn == null) return;
|
|
|
- if (filesToActOn.Any()) ResetMultipleFiles(filesToActOn);
|
|
|
- else ForcePull();
|
|
|
- });
|
|
|
+ ConflictResolutionWindow.ShowWindow(this, analysisResult.ConflictingFiles);
|
|
|
}
|
|
|
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();
|
|
|
- });
|
|
|
+ PerformSafePullWithLock();
|
|
|
}
|
|
|
})
|
|
|
.Catch(ex => {
|
|
|
HandleOperationError(ex);
|
|
|
FinishOperation();
|
|
|
- });
|
|
|
+ })
|
|
|
+ .Finally(FinishOperation);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void PerformSafePullWithLock()
|
|
|
+ {
|
|
|
+ StartOperation("Pulling changes...");
|
|
|
+ EditorApplication.LockReloadAssemblies();
|
|
|
+ GitService.PerformSafePull()
|
|
|
+ .Then(successMessage =>
|
|
|
+ {
|
|
|
+ InfoMessage = successMessage;
|
|
|
+ Refresh();
|
|
|
+ })
|
|
|
+ .Catch(ex => {
|
|
|
+ HandleOperationError(ex);
|
|
|
+ FinishOperation();
|
|
|
+ })
|
|
|
+ .Finally(EditorApplication.UnlockReloadAssemblies);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void ResetSingleConflictingFile(string filePath, Action onResetComplete)
|
|
|
+ {
|
|
|
+ var change = GitService.GetChangeForFile(filePath);
|
|
|
+ if (change == null)
|
|
|
+ {
|
|
|
+ ErrorMessage = $"Could not find file '{filePath}' to reset.";
|
|
|
+ onResetComplete?.Invoke();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ GitService.ResetFileChanges(change)
|
|
|
+ .Then(successMessage =>
|
|
|
+ {
|
|
|
+ InfoMessage = successMessage;
|
|
|
+ })
|
|
|
+ .Catch(HandleOperationError)
|
|
|
+ .Finally(onResetComplete);
|
|
|
}
|
|
|
|
|
|
public void CommitAndPush(string commitMessage)
|
|
@@ -122,11 +164,47 @@ namespace Terra.Arbitrator.GUI
|
|
|
FinishOperation();
|
|
|
});
|
|
|
}
|
|
|
+
|
|
|
+ public void SetSortColumn(SortColumn newColumn)
|
|
|
+ {
|
|
|
+ // If it's already the active column, do nothing.
|
|
|
+ if (CurrentSortColumn == newColumn) return;
|
|
|
+
|
|
|
+ CurrentSortColumn = newColumn;
|
|
|
+ ApplyGrouping();
|
|
|
+ _requestRepaint?.Invoke();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ApplyGrouping()
|
|
|
+ {
|
|
|
+ if (_changes == null || !_changes.Any()) return;
|
|
|
+
|
|
|
+ _changes = CurrentSortColumn switch
|
|
|
+ {
|
|
|
+ SortColumn.Commit => _changes.OrderByDescending(c => c.IsSelectedForCommit).ThenBy(c => c.FilePath).ToList(),
|
|
|
+ SortColumn.Status => _changes.OrderBy(c => GetStatusSortPriority(c.Status)).ThenBy(c => c.FilePath).ToList(),
|
|
|
+ SortColumn.FilePath => _changes.OrderBy(c => c.FilePath).ToList(),
|
|
|
+ _ => _changes
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private int GetStatusSortPriority(LibGit2Sharp.ChangeKind status)
|
|
|
+ {
|
|
|
+ return status switch
|
|
|
+ {
|
|
|
+ LibGit2Sharp.ChangeKind.Conflicted => -1, // Always show conflicts on top
|
|
|
+ LibGit2Sharp.ChangeKind.Modified => 0,
|
|
|
+ LibGit2Sharp.ChangeKind.Added => 1,
|
|
|
+ LibGit2Sharp.ChangeKind.Deleted => 2,
|
|
|
+ LibGit2Sharp.ChangeKind.Renamed => 3,
|
|
|
+ _ => 99
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
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.");
|
|
|
+ var userConfirmed = _displayDialog("Confirm Reset", $"Are you sure you want to revert all local changes to '{change.FilePath}'? This action cannot be undone.", "Yes, Revert", "Cancel");
|
|
|
if (!userConfirmed) return;
|
|
|
|
|
|
StartOperation($"Resetting {change.FilePath}...");
|
|
@@ -179,35 +257,25 @@ namespace Terra.Arbitrator.GUI
|
|
|
.Catch(HandleOperationError)
|
|
|
.Finally(Refresh);
|
|
|
}
|
|
|
-
|
|
|
- public void ResetAll()
|
|
|
+
|
|
|
+ public void ResetSelected()
|
|
|
{
|
|
|
- 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...");
|
|
|
+ var selectedFiles = _changes.Where(c => c.IsSelectedForCommit).ToList();
|
|
|
+ if (!selectedFiles.Any()) return;
|
|
|
|
|
|
- GitService.ResetAllChanges()
|
|
|
- .Then(successMessage =>
|
|
|
- {
|
|
|
- InfoMessage = successMessage;
|
|
|
- })
|
|
|
- .Catch(HandleOperationError)
|
|
|
- .Finally(Refresh);
|
|
|
+ var fileList = string.Join("\n - ", selectedFiles.Select(f => f.FilePath));
|
|
|
+ if (!_displayDialog("Confirm Reset Selected", $"Are you sure you want to revert changes for the following {selectedFiles.Count} file(s)?\n\n - {fileList}", "Yes, Revert Selected", "Cancel")) return;
|
|
|
+
|
|
|
+ var pathsToReset = selectedFiles.Select(c => c.FilePath).ToList();
|
|
|
+ ResetMultipleFiles(pathsToReset);
|
|
|
}
|
|
|
|
|
|
public void SetAllSelection(bool selected)
|
|
|
{
|
|
|
if (_changes == null) return;
|
|
|
- foreach (var change in _changes)
|
|
|
+ foreach (var change in _changes.Where(change => change.Status != LibGit2Sharp.ChangeKind.Conflicted))
|
|
|
{
|
|
|
- if (change.Status != LibGit2Sharp.ChangeKind.Conflicted)
|
|
|
- {
|
|
|
- change.IsSelectedForCommit = selected;
|
|
|
- }
|
|
|
+ change.IsSelectedForCommit = selected;
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -227,6 +295,25 @@ namespace Terra.Arbitrator.GUI
|
|
|
return GitService.CompareLocalToRemote();
|
|
|
}
|
|
|
|
|
|
+ private IPromise<int?> FetchUpstreamStep(List<GitChange> changes)
|
|
|
+ {
|
|
|
+ _changes = changes;
|
|
|
+ IsInConflictState = _changes.Any(c => c.Status == LibGit2Sharp.ChangeKind.Conflicted);
|
|
|
+
|
|
|
+ return IsInConflictState ?
|
|
|
+ // If in conflict, we can't pull, so we don't need to check. Return a resolved promise.
|
|
|
+ new Promise<int?>((resolve, _) => resolve(0)) : GitService.GetUpstreamAheadBy();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void FinalizeRefresh(int? pullCount)
|
|
|
+ {
|
|
|
+ CommitsToPull = pullCount ?? 0;
|
|
|
+ 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)
|
|
@@ -281,5 +368,36 @@ namespace Terra.Arbitrator.GUI
|
|
|
Refresh();
|
|
|
});
|
|
|
}
|
|
|
+
|
|
|
+ private bool CancelOperationIfUnsavedScenes()
|
|
|
+ {
|
|
|
+ var isAnySceneDirty = false;
|
|
|
+ for (var i = 0; i < EditorSceneManager.sceneCount; i++)
|
|
|
+ {
|
|
|
+ var scene = EditorSceneManager.GetSceneAt(i);
|
|
|
+ if (!scene.isDirty) continue;
|
|
|
+ isAnySceneDirty = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isAnySceneDirty)
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ var userChoice = _promptForUnsavedChanges();
|
|
|
+
|
|
|
+ switch (userChoice)
|
|
|
+ {
|
|
|
+ case UserAction.SaveAndProceed:
|
|
|
+ EditorSceneManager.SaveOpenScenes();
|
|
|
+ return false;
|
|
|
+ case UserAction.Proceed:
|
|
|
+ return false;
|
|
|
+ case UserAction.Cancel:
|
|
|
+ default:
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|