Explorar o código

Sujith :) ->
1. Basic implementations of ArbitratorWindow
2. Commit push does not work

Sujith:) hai 1 mes
pai
achega
182deedfa9

+ 0 - 147
Assets/Arbitrator/Editor/Arbitrator.cs

@@ -1,147 +0,0 @@
-// Copyright (c) 2025 TerraByte Inc.
-//
-// This script creates a custom Unity Editor window called "Arbitrator" to compare 
-// local Git changes with the tracked remote branch using the LibGit2Sharp library.
-//
-// HOW TO USE:
-// 1. Ensure you have manually installed the LibGit2Sharp v0.27.0 package.
-// 2. Create an "Editor" folder in your Assets directory if you don't have one.
-// 3. Save this script as "Arbitrator.cs" inside the "Editor" folder.
-// 4. In Unity, open the window from the top menu: Terra > Arbitrator.
-// 5. Click the "Compare with Cloud" button. Results will appear in the console.
-
-using System;
-using System.IO;
-using UnityEngine;
-using UnityEditor;
-using LibGit2Sharp;
-
-namespace Terra.Arbitrator
-{
-    public class Arbitrator : EditorWindow
-    {
-        // Creates a menu item in the Unity Editor to open this window.
-        [MenuItem("Terra/Arbitrator")]
-        public static void ShowWindow()
-        {
-            // Get an existing open window or if none, make a new one.
-            GetWindow<Arbitrator>("Arbitrator");
-        }
-
-        // This method is called to draw the contents of the editor window.
-        private void OnGUI()
-        {   
-            // Add some descriptive text to the window.
-            EditorGUILayout.LabelField("Compare local changes against the cloud.", EditorStyles.boldLabel);
-            EditorGUILayout.HelpBox("This tool will fetch the latest state from the remote repository and show you which files have been added, modified, or deleted locally.", MessageType.Info);
-
-            // Render a button. If the user clicks it, call our main logic function.
-            if (GUILayout.Button("Compare with Cloud", GUILayout.Height(40)))
-            {
-                CompareLocalToRemote();
-            }
-        }
-
-        /// <summary>
-        /// The core function that performs the fetch and diff operations.
-        /// </summary>
-        private static void CompareLocalToRemote()
-        {
-            // The project root is one level above the "Assets" directory.
-            var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
-
-            try
-            {
-                // Use a 'using' statement to ensure the repository object is properly disposed of.
-                // This opens the repository at the root of your Unity project.
-                using var repo = new Repository(projectRoot);
-                Debug.Log("Repository opened successfully.");
-
-                // --- Step 1: Fetch the latest state from the remote ---
-                // Get the primary remote, which is typically named "origin".
-                var remote = repo.Network.Remotes["origin"];
-                if (remote == null)
-                {
-                    Debug.LogError("No remote named 'origin' was found. Please configure a remote for your repository.");
-                    return;
-                }
-
-                // IMPORTANT: For private repositories, you'll need to provide credentials.
-                // This is a more advanced topic involving Personal Access Tokens (PATs).
-                var fetchOptions = new FetchOptions();
-                    
-                Debug.Log($"Fetching latest changes from '{remote.Name}' at {remote.Url}...");
-                Commands.Fetch(repo, remote.Name, Array.Empty<string>(), fetchOptions, null);
-                Debug.Log("Fetch complete.");
-
-                // --- Step 2: Identify branches to compare ---
-                var currentBranch = repo.Head;
-                var remoteBranch = currentBranch.TrackedBranch;
-
-                if (remoteBranch == null || !remoteBranch.IsRemote)
-                {
-                    Debug.LogError($"The current branch '{currentBranch.FriendlyName}' is not tracking a remote branch. Set the upstream branch using 'git branch --set-upstream-to=origin/{currentBranch.FriendlyName}'.");
-                    return;
-                }
-                    
-                Debug.Log($"Comparing local '{currentBranch.FriendlyName}' against remote '{remoteBranch.FriendlyName}'.");
-
-                // --- Step 3: Perform the Diff ---
-                // We compare the tree from the latest commit on the remote branch
-                // against the files currently in your working directory.
-                var remoteTree = remoteBranch.Tip.Tree;
-
-                var changes = repo.Diff.Compare<TreeChanges>(remoteTree, DiffTargets.Index | DiffTargets.WorkingDirectory);
-
-                // --- Step 4: Log the Results ---
-                if (changes.Count == 0)
-                {
-                    Debug.Log("--- STATUS: You are up to date! No differences found with the remote branch. ---");
-                }
-                else
-                {
-                    Debug.Log($"--- STATUS: Found {changes.Count} changes. ---");
-                    foreach (var change in changes)
-                    {
-                        // Log the status (Added, Deleted, Modified) and the file path.
-                        // The color-coding helps to quickly see the status.
-                        switch (change.Status)
-                        {
-                            case ChangeKind.Added:
-                                Debug.Log($"<color=green>ADDED: {change.Path}</color>");
-                                break;
-                            case ChangeKind.Deleted:
-                                Debug.Log($"<color=red>DELETED: {change.Path}</color>");
-                                break;
-                            case ChangeKind.Modified:
-                                Debug.Log($"<color=orange>MODIFIED: {change.Path}</color>");
-                                break;
-                            case ChangeKind.Unmodified:
-                            case ChangeKind.Renamed:
-                            case ChangeKind.Copied:
-                            case ChangeKind.Ignored:
-                            case ChangeKind.Untracked:
-                            case ChangeKind.TypeChanged:
-                            case ChangeKind.Unreadable:
-                            case ChangeKind.Conflicted:
-                            default:
-                                Debug.Log($"{change.Status.ToString().ToUpper()}: {change.Path}");
-                                break;
-                        }
-                    }
-                    Debug.Log("--- End of Report ---");
-                }
-            }
-            catch (RepositoryNotFoundException)
-            {
-                Debug.LogError("Error: This project is not a Git repository or the .git folder is missing. Please initialize a repository first.");
-            }
-            catch (Exception ex)
-            {
-                // Catch any other exceptions that might occur (e.g., network issues during fetch).
-                Debug.LogError($"An unexpected error occurred: {ex.Message}");
-                Debug.LogException(ex);
-            }
-        }
-    }
-}

+ 294 - 0
Assets/Arbitrator/Editor/ArbitratorWindow.cs

@@ -0,0 +1,294 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// This script creates a custom Unity Editor window called "Arbitrator" to compare 
+// local Git changes with the tracked remote branch using the LibGit2Sharp library.
+//
+// HOW TO USE:
+// 1. Ensure you have manually installed the LibGit2Sharp v0.27.0 package.
+// 2. Create an "Editor" folder in your Assets directory if you don't have one.
+// 3. Save this script as "Arbitrator.cs" inside the "Editor" folder.
+// 4. In Unity, open the window from the top menu: Terra > Arbitrator.
+// 5. Click the "Compare with Cloud" button. Results will appear in the console.
+
+using System.Linq;
+using UnityEngine;
+using UnityEditor;
+using LibGit2Sharp;
+using System.Collections.Generic;
+
+namespace Terra.Arbitrator
+{
+    public class ArbitratorWindow : EditorWindow
+    {
+        private List<GitChange> _changes;
+        private string _commitMessage = "";
+        private Vector2 _scrollPosition;
+        private string _infoMessage;
+        private string _errorMessage;
+        private bool _isLoading;
+        private string _loadingMessage = "";
+
+        [MenuItem("Terra/Changes")]
+        public static void ShowWindow()
+        {
+            var window = GetWindow<ArbitratorWindow>();
+            window.titleContent = new GUIContent("Changes", EditorGUIUtility.IconContent("d_UnityEditor.VersionControl").image);
+        }
+        
+        /// <summary>
+        /// Called when the window is enabled. This triggers an automatic refresh
+        /// when the window is opened or after scripts are recompiled.
+        /// </summary>
+        private void OnEnable()
+        {
+            HandleCompare();
+        }
+
+        private void OnGUI()
+        {
+            // --- Top Toolbar ---
+            DrawToolbar();
+            
+            EditorGUILayout.Space();
+
+            // --- Message Display Area ---
+            if (!string.IsNullOrEmpty(_errorMessage))
+            {
+                EditorGUILayout.HelpBox(_errorMessage, MessageType.Error);
+            }
+            else if (!string.IsNullOrEmpty(_infoMessage) && !_isLoading)
+            {
+                // Only show an info message if not loading, to prevent flicker
+                EditorGUILayout.HelpBox(_infoMessage, MessageType.Info);
+            }
+
+            // --- Main Content ---
+            if (_isLoading)
+            {
+                // You can add a more prominent loading indicator here if you wish
+            }
+            else if (_changes is { Count: > 0 })
+            {
+                DrawChangesList();
+                DrawCommitSection();
+            }
+        }
+        
+        private void ClearMessages()
+        {
+            _errorMessage = null;
+            _infoMessage = null;
+        }
+        
+        /// <summary>
+        /// Draws the top menu bar for actions like refreshing.
+        /// </summary>
+        private void DrawToolbar()
+        {
+            EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+
+            // The refresh button is now on the toolbar
+            if (GUILayout.Button(new GUIContent("Refresh", EditorGUIUtility.IconContent("Refresh").image, "Fetches the latest status from the remote."), EditorStyles.toolbarButton, GUILayout.Width(80)))
+            {
+                HandleCompare();
+            }
+            
+            // This pushes everything that comes after it to the right.
+            GUILayout.FlexibleSpace();
+
+            // The loading message will appear on the right side of the toolbar.
+            if (_isLoading)
+            {
+                GUILayout.Label(_loadingMessage);
+            }
+            
+            // Future: Add a dropdown menu for filters or settings here.
+            // If (GUILayout.Button("Filters", EditorStyles.toolbarDropDown)) { ... }
+
+            EditorGUILayout.EndHorizontal();
+        }
+
+        private void HandleCompare()
+        {
+            _isLoading = true;
+            _loadingMessage = "Comparing with remote repository...";
+            ClearMessages();
+            _changes = null;
+
+            GitService.CompareLocalToRemote()
+                .Then(result => {
+                    _changes = result;
+                    if (_changes.Count == 0)
+                    {
+                        _infoMessage = "You are up-to-date! No local changes detected.";
+                    }
+                })
+                .Catch(ex => {
+                    _errorMessage = $"Comparison Failed: {ex.Message}";
+                })
+                .Finally(() => {
+                    _isLoading = false;
+                    Repaint(); // Redraw the window with the new state
+                });
+        }
+
+        private void HandleCommitAndPush()
+        {
+            _isLoading = true;
+            _loadingMessage = "Staging, committing, and pushing files...";
+            ClearMessages();
+
+            var selectedFiles = _changes.Where(c => c.IsSelectedForCommit).ToList();
+
+            GitService.CommitAndPush(selectedFiles, _commitMessage)
+                .Then(successMessage => {
+                    _infoMessage = successMessage;
+                    _commitMessage = ""; // Clear message on success
+                    _changes = null; // Clear the list, forcing a refresh
+                    HandleCompare(); // Automatically refresh to confirm
+                })
+                .Catch(ex => {
+                    _errorMessage = $"Push Failed: {ex.Message}";
+                })
+                .Finally(() => {
+                    _isLoading = false;
+                    // Repaint is handled by the chained HandleCompare call on success
+                    if (!string.IsNullOrEmpty(_errorMessage)) Repaint();
+                });
+        }
+        
+        private void HandleResetFile(GitChange change)
+        {
+            if (_isLoading) return;
+            _isLoading = true;
+            _loadingMessage = $"Generating diff for {change.FilePath}...";
+            ClearMessages();
+            Repaint();
+
+            // Step 1: Get the diff content for the file.
+            GitService.GetFileDiff(change)
+                .Then(diffContent =>
+                {
+                    // This callback runs on the main thread when the diff is ready.
+                    // Step 2: Show the modal diff window.
+                    DiffWindow.ShowWindow(change.FilePath, diffContent, wasConfirmed =>
+                    {
+                        // This callback runs after the diff window is closed.
+                        if (!wasConfirmed)
+                        {
+                            _isLoading = false; // User cancelled.
+                            Repaint();
+                            return;
+                        }
+
+                        // Step 3: User confirmed. Proceed to reset the file.
+                        _loadingMessage = $"Resetting {change.FilePath}...";
+                        Repaint();
+
+                        GitService.ResetFileChanges(change)
+                            .Then(successMessage => {
+                                _infoMessage = successMessage;
+                                HandleCompare(); // Refresh the main list.
+                            })
+                            .Catch(ex => {
+                                _errorMessage = $"Reset Failed: {ex.Message}";
+                                _isLoading = false;
+                                Repaint();
+                            });
+                    });
+                })
+                .Catch(ex =>
+                {
+                    // This runs if getting the diff itself failed.
+                    _errorMessage = $"Could not generate diff: {ex.Message}";
+                    _isLoading = false;
+                    Repaint();
+                })
+                .Finally(HandleCompare);
+        }
+
+        /// <summary>
+        /// Draws the multi-column list of changed files.
+        /// </summary>
+        private void DrawChangesList()
+        {
+            // --- Draw Header ---
+            EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
+            EditorGUILayout.LabelField("Commit", GUILayout.Width(45));
+            EditorGUILayout.LabelField("Status", GUILayout.Width(50));
+            EditorGUILayout.LabelField("File Path");
+            EditorGUILayout.LabelField("Actions", GUILayout.Width(55));
+            EditorGUILayout.EndHorizontal();
+
+            // --- Draw Scrollable List ---
+            _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.ExpandHeight(true));
+            foreach (var change in _changes)
+            {
+                EditorGUILayout.BeginHorizontal();
+
+                // Column 1: Toggle Box
+                change.IsSelectedForCommit = EditorGUILayout.Toggle(change.IsSelectedForCommit, GUILayout.Width(45));
+
+                // Column 2: Status
+                string status;
+                Color statusColor;
+                switch (change.Status)
+                {
+                    case ChangeKind.Added: status = "[+]"; statusColor = Color.green; break;
+                    case ChangeKind.Deleted: status = "[-]"; statusColor = Color.red; break;
+                    case ChangeKind.Modified: status = "[M]"; statusColor = new Color(1.0f, 0.6f, 0.0f); break;
+                    case ChangeKind.Unmodified:
+                    case ChangeKind.Renamed:
+                    case ChangeKind.Copied:
+                    case ChangeKind.Ignored:
+                    case ChangeKind.Untracked:
+                    case ChangeKind.TypeChanged:
+                    case ChangeKind.Unreadable:
+                    case ChangeKind.Conflicted:
+                        status = "[C]"; statusColor = new Color(0.5f, 0.5f, 0.5f, 0.3f); break;
+                    default: status = "[?]"; statusColor = Color.white; break;
+                }
+                var originalColor = GUI.color;
+                GUI.color = statusColor;
+                EditorGUILayout.LabelField(new GUIContent(status, change.Status.ToString()), GUILayout.Width(50));
+                GUI.color = originalColor;
+
+                // Column 3: File Path
+                EditorGUILayout.LabelField(new GUIContent(change.FilePath, change.FilePath));
+
+                // Column 4: Reset Button
+                EditorGUI.BeginDisabledGroup(_isLoading);
+                if (GUILayout.Button(new GUIContent("Reset", "Revert changes for this file"), GUILayout.Width(55)))
+                {
+                    // Defers the action to avoid GUI layout errors
+                    EditorApplication.delayCall += () => HandleResetFile(change);
+                }
+                EditorGUI.EndDisabledGroup();
+                
+                EditorGUILayout.EndHorizontal();
+            }
+            EditorGUILayout.EndScrollView();
+        }
+        
+        private void DrawCommitSection()
+        {
+            EditorGUILayout.Space(10);
+            EditorGUILayout.LabelField("Commit & Push", EditorStyles.boldLabel);
+            _commitMessage = EditorGUILayout.TextArea(_commitMessage, GUILayout.Height(60), GUILayout.ExpandWidth(true));
+
+            var isPushDisabled = string.IsNullOrWhiteSpace(_commitMessage) || !_changes.Any(c => c.IsSelectedForCommit);
+
+            EditorGUI.BeginDisabledGroup(isPushDisabled);
+            if (GUILayout.Button("Commit & Push Selected Files", GUILayout.Height(40)))
+            {
+                HandleCommitAndPush();
+            }
+            EditorGUI.EndDisabledGroup();
+            
+            if (isPushDisabled)
+            {
+                EditorGUILayout.HelpBox("Please enter a commit message and select at least one file.", MessageType.Warning);
+            }
+        }
+    }
+}

Assets/Arbitrator/Editor/Arbitrator.cs.meta → Assets/Arbitrator/Editor/ArbitratorWindow.cs.meta


+ 110 - 0
Assets/Arbitrator/Editor/DiffWindow.cs

@@ -0,0 +1,110 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// A modal editor window for displaying text differences (diffs) and
+// confirming an action with the user.
+
+using System;
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+
+namespace Terra.Arbitrator
+{
+    public class DiffWindow : EditorWindow
+    {
+        private string _filePath;
+        private string _diffContent;
+        private Action<bool> _onCloseCallback;
+        private Vector2 _scrollPosition;
+        private bool _callbackInvoked = false;
+
+        /// <summary>
+        /// Shows a modal window to display the diff and get user confirmation.
+        /// </summary>
+        /// <param name="filePath">The path of the file being diffed.</param>
+        /// <param name="diffContent">The diff text to display.</param>
+        /// <param name="onCloseCallback">A callback that returns true if the user confirmed, otherwise false.</param>
+        public static void ShowWindow(string filePath, string diffContent, Action<bool> onCloseCallback)
+        {
+            DiffWindow window = GetWindow<DiffWindow>(true, "Diff Viewer", true);
+            window.titleContent = new GUIContent($"Diff: {Path.GetFileName(filePath)}");
+            window.minSize = new Vector2(700, 500);
+            window._filePath = filePath;
+            window._diffContent = diffContent;
+            window._onCloseCallback = onCloseCallback;
+            window._callbackInvoked = false;
+            window.ShowModalUtility(); // Show as a blocking modal window
+        }
+
+        private void OnGUI()
+        {
+            EditorGUILayout.LabelField(_filePath, EditorStyles.boldLabel);
+
+            // Create a custom style for the diff text for better readability
+            var diffStyle = new GUIStyle(EditorStyles.textArea)
+            {
+                richText = true,
+                wordWrap = false
+            };
+            
+            _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, EditorStyles.helpBox, GUILayout.ExpandHeight(true));
+            EditorGUILayout.SelectableLabel(GetColoredDiff(_diffContent), diffStyle, GUILayout.ExpandHeight(true));
+            EditorGUILayout.EndScrollView();
+
+            EditorGUILayout.Space();
+            EditorGUILayout.HelpBox("Review the changes above. Green lines are additions, red lines are deletions. Reverting will permanently discard these changes.", MessageType.Warning);
+
+            EditorGUILayout.BeginHorizontal();
+            GUILayout.FlexibleSpace();
+            if (GUILayout.Button("Revert Changes", GUILayout.Height(30), GUILayout.Width(150)))
+            {
+                _callbackInvoked = true;
+                _onCloseCallback?.Invoke(true);
+                Close();
+            }
+            if (GUILayout.Button("Cancel", GUILayout.Height(30), GUILayout.Width(120)))
+            {
+                // Let OnDestroy handle the callback
+                Close();
+            }
+            EditorGUILayout.EndHorizontal();
+        }
+
+        private static string GetColoredDiff(string rawDiff)
+        {
+            var coloredDiff = "";
+            var lines = rawDiff.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+            foreach (var line in lines)
+            {
+                if (line.StartsWith("+"))
+                {
+                    coloredDiff += $"<color=green>{line}</color>\n";
+                }
+                else if (line.StartsWith("-"))
+                {
+                    coloredDiff += $"<color=red>{line}</color>\n";
+                }
+                else if (line.StartsWith("@@"))
+                {
+                     coloredDiff += $"<color=cyan>{line}</color>\n";
+                }
+                else
+                {
+                    coloredDiff += $"{line}\n";
+                }
+            }
+            return coloredDiff;
+        }
+
+        private void OnDestroy()
+        {
+            // This is called when the window is closed by any means.
+            // If the callback hasn't been invoked yet (i.e., user clicked 'X' or 'Cancel'),
+            // we invoke it with 'false' to signify cancellation.
+            if (!_callbackInvoked)
+            {
+                _onCloseCallback?.Invoke(false);
+            }
+        }
+    }
+}

+ 3 - 0
Assets/Arbitrator/Editor/DiffWindow.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 925fa0607b96465b89370d83f2668c89
+timeCreated: 1750158026

+ 35 - 0
Assets/Arbitrator/Editor/GitChange.cs

@@ -0,0 +1,35 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// This script defines the data model for a single Git change.
+// An object of this class holds information about a file that has been
+// added, modified, or deleted.
+//
+// HOW TO USE:
+// 1. Place this script inside the "Editor" folder.
+// 2. This class is used by GitService.cs and ArbitratorWindow.cs.
+
+using LibGit2Sharp;
+
+namespace Terra.Arbitrator
+{
+    /// <summary>
+    /// A data container for a single file change detected by Git.
+    /// </summary>
+    public class GitChange
+    {
+        public string FilePath { get; private set; }
+        public ChangeKind Status { get; private set; }
+        
+        /// <summary>
+        /// Represents whether the user has checked the box for this file in the UI.
+        /// </summary>
+        public bool IsSelectedForCommit { get; set; }
+
+        public GitChange(string filePath, ChangeKind status)
+        {
+            FilePath = filePath;
+            Status = status;
+            IsSelectedForCommit = true; // Default to select
+        }
+    }
+}

+ 3 - 0
Assets/Arbitrator/Editor/GitChange.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2c8f660bd6e24dccadba7ab1148c5ce9
+timeCreated: 1750142745

+ 185 - 0
Assets/Arbitrator/Editor/GitService.cs

@@ -0,0 +1,185 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// This script contains the core business logic for interacting with the Git
+// repository. All public methods are asynchronous and return a promise.
+
+using System;
+using System.IO;
+using UnityEngine;
+using LibGit2Sharp;
+using Terra.Arbitrator.Promises;
+using System.Collections.Generic;
+
+namespace Terra.Arbitrator
+{
+    public static class GitService
+    {
+        // Public method that returns the promise
+        public static IPromise<List<GitChange>> CompareLocalToRemote()
+        {
+            return new Promise<List<GitChange>>(CompareExecutor);
+        }
+        
+        // Public method that returns the promise
+        public static IPromise<string> CommitAndPush(List<GitChange> changesToCommit, string commitMessage)
+        {
+            // Use a lambda here to pass arguments to the executor method
+            return new Promise<string>((resolve, reject) => 
+                CommitAndPushExecutor(resolve, reject, changesToCommit, commitMessage));
+        }
+        
+        // Creates a promise to revert a single file to its HEAD revision.
+        public static IPromise<string> ResetFileChanges(GitChange changeToReset)
+        {
+            return new Promise<string>((resolve, reject) => 
+                ResetFileExecutor(resolve, reject, changeToReset));
+        }
+        
+        // Creates a promise to get the diff patch for a single file against HEAD.
+        public static IPromise<string> GetFileDiff(GitChange change)
+        {
+            return new Promise<string>((resolve, reject) => GetFileDiffExecutor(resolve, reject, change));
+        }
+
+        /// <summary>
+        /// The private logic for the CompareLocalToRemote promise.
+        /// This code is executed on a background thread by the Promise constructor.
+        /// </summary>
+        private static void CompareExecutor(Action<List<GitChange>> resolve, Action<Exception> reject)
+        {
+            try
+            {
+                var changes = new List<GitChange>();
+                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                using var repo = new Repository(projectRoot);
+
+                var remote = repo.Network.Remotes["origin"];
+                if (remote == null) throw new Exception("No remote named 'origin' was found.");
+                
+                Commands.Fetch(repo, remote.Name, Array.Empty<string>(), new FetchOptions(), "Arbitrator fetch");
+                
+                var remoteBranch = repo.Head.TrackedBranch;
+                if (remoteBranch == null || !remoteBranch.IsRemote)
+                {
+                    throw new Exception($"Current branch '{repo.Head.FriendlyName}' is not tracking a remote branch.");
+                }
+
+                var diff = repo.Diff.Compare<TreeChanges>(remoteBranch.Tip.Tree, DiffTargets.Index | DiffTargets.WorkingDirectory);
+
+                foreach (var entry in diff)
+                {
+                    if (entry.Status == ChangeKind.Added || entry.Status == ChangeKind.Deleted || entry.Status == ChangeKind.Modified)
+                    {
+                        changes.Add(new GitChange(entry.Path, entry.Status));
+                    }
+                }
+                
+                resolve(changes); // Success
+            }
+            catch (Exception ex)
+            {
+                reject(ex); // Failure
+            }
+        }
+
+        /// <summary>
+        /// The private logic for the CommitAndPush promise.
+        /// This code is executed on a background thread.
+        /// </summary>
+        private static void CommitAndPushExecutor(Action<string> resolve, Action<Exception> reject, List<GitChange> changesToCommit, string commitMessage)
+        {
+            try
+            {
+                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                using var repo = new Repository(projectRoot);
+
+                var remote = repo.Network.Remotes["origin"];
+                if (remote == null) throw new Exception("No remote named 'origin' found.");
+                
+                Commands.Fetch(repo, remote.Name, Array.Empty<string>(), new FetchOptions(), "Arbitrator pre-push fetch");
+
+                var trackingDetails = repo.Head.TrackingDetails;
+                if (trackingDetails.BehindBy > 0)
+                {
+                    throw new Exception($"Push aborted. There are {trackingDetails.BehindBy.Value} incoming changes on the remote. Please pull first.");
+                }
+
+                foreach (var change in changesToCommit)
+                {
+                    if (change.Status == ChangeKind.Deleted) Commands.Remove(repo, change.FilePath);
+                    else Commands.Stage(repo, change.FilePath);
+                }
+
+                var author = new Signature("Arbitrator Tool User", "arbitrator@terabyte.com", DateTimeOffset.Now);
+                repo.Commit(commitMessage, author, author);
+
+                repo.Network.Push(repo.Head, new PushOptions());
+                
+                resolve("Successfully committed and pushed changes!"); // Success
+            }
+            catch (Exception ex)
+            {
+                reject(ex); // Failure
+            }
+        }
+        
+        /// <summary>
+        /// The private logic for resetting a file's changes.
+        /// This is executed on a background thread.
+        /// </summary>
+        private static void ResetFileExecutor(Action<string> resolve, Action<Exception> reject, GitChange changeToReset)
+        {
+            try
+            {
+                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                using var repo = new Repository(projectRoot);
+
+                // For 'Added' files, checking out doesn't remove them. We need to unstage and delete.
+                if (changeToReset.Status == ChangeKind.Added)
+                {
+                    // Unstage the file from the index.
+                    Commands.Unstage(repo, changeToReset.FilePath);
+                    
+                    // And delete it from the working directory.
+                    if (projectRoot != null)
+                    {
+                        var fullPath = Path.Combine(projectRoot, changeToReset.FilePath);
+                        if (File.Exists(fullPath))
+                        {
+                            File.Delete(fullPath);
+                        }
+                    }
+                }
+                else
+                {
+                    // For Modified or Deleted files, CheckoutPaths is the correct command to revert them.
+                    repo.CheckoutPaths(repo.Head.Tip.Sha, new[] { changeToReset.FilePath }, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
+                }
+
+                resolve($"Successfully reset changes for '{changeToReset.FilePath}'");
+            }
+            catch (Exception ex)
+            {
+                reject(ex);
+            }
+        }
+        
+        private static void GetFileDiffExecutor(Action<string> resolve, Action<Exception> reject, GitChange change)
+        {
+            try
+            {
+                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                using var repo = new Repository(projectRoot);
+
+                // Use Compare() against the HEAD commit to get the patch for the specific file.
+                var diff = repo.Diff.Compare<Patch>(new[] { change.FilePath }, true);
+                
+                resolve(diff.Content);
+            }
+            catch(Exception ex)
+            {
+                reject(ex);
+            }
+        }
+    }
+}

+ 3 - 0
Assets/Arbitrator/Editor/GitService.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 9435df7b84314debaa6cadff8c574a89
+timeCreated: 1750142727

+ 3 - 0
Assets/Arbitrator/Editor/Promises.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 3d9429b59f6b4b0b846de2b61942e331
+timeCreated: 1750142605

+ 30 - 0
Assets/Arbitrator/Editor/Promises/IPromise.cs

@@ -0,0 +1,30 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// Defines the public interface for a Promise, which represents the eventual
+// completion (or failure) of an asynchronous operation and its resulting value.
+// This decouples the consumer from the implementation.
+
+using System;
+
+namespace Terra.Arbitrator.Promises
+{
+    public enum PromiseState { Pending, Resolved, Rejected }
+
+    public interface IPromise<out TResult>
+    {
+        /// <summary>
+        /// Attaches a callback that will execute when the promise is successfully resolved.
+        /// </summary>
+        IPromise<TResult> Then(Action<TResult> onResolved);
+        
+        /// <summary>
+        /// Attaches a callback that will execute when the promise is rejected.
+        /// </summary>
+        IPromise<TResult> Catch(Action<Exception> onRejected);
+        
+        /// <summary>
+        /// Attaches a callback that will execute when the promise is settled (either resolved or rejected).
+        /// </summary>
+        void Finally(Action onFinally);
+    }
+}

+ 3 - 0
Assets/Arbitrator/Editor/Promises/IPromise.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 9a336fffd8174d4fa157f72466eef79e
+timeCreated: 1750142645

+ 19 - 0
Assets/Arbitrator/Editor/Promises/ITrackablePromise.cs

@@ -0,0 +1,19 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// A non-generic interface that allows the PromiseManager to track and
+// tick promises without needing to know their specific result type.
+
+namespace Terra.Arbitrator.Promises
+{
+    /// <summary>
+    /// Represents a promise that can be ticked by the PromiseManager.
+    /// </summary>
+    public interface ITrackablePromise
+    {
+        /// <summary>
+        /// Executes pending callbacks if the promise is settled.
+        /// </summary>
+        /// <returns>True if the promise is settled (resolved or rejected), otherwise false.</returns>
+        bool Tick();
+    }
+}

+ 3 - 0
Assets/Arbitrator/Editor/Promises/ITrackablePromise.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ef57a2323d9f40ad971f77716485650e
+timeCreated: 1750144954

+ 118 - 0
Assets/Arbitrator/Editor/Promises/Promise.cs

@@ -0,0 +1,118 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// The concrete implementation of the IPromise interface.
+// It manages the state and execution of an asynchronous operation.
+
+using System;
+using System.Threading;
+
+namespace Terra.Arbitrator.Promises
+{
+    public class Promise<TResult> : IPromise<TResult>, ITrackablePromise
+    {
+        public PromiseState State { get; private set; }
+        private TResult _resolvedValue;
+        private Exception _rejectedException;
+
+        // Callbacks to be executed on the main thread
+        private Action<TResult> _onResolvedCallback;
+        private Action<Exception> _onRejectedCallback;
+        private Action _onFinallyCallback;
+        
+        // Thread-safe flags
+        private volatile bool _isResolved;
+        private volatile bool _isRejected;
+
+        /// <summary>
+        /// Creates a new promise and starts its execution on a background thread.
+        /// </summary>
+        /// <param name="executor">The function to execute, which takes resolve and reject actions.</param>
+        public Promise(Action<Action<TResult>, Action<Exception>> executor)
+        {
+            _isRejected = false;
+            State = PromiseState.Pending;
+            // The manager will track this promise until it's settled.
+            PromiseManager.Track(this);
+
+            ThreadPool.QueueUserWorkItem(_ =>
+            {
+                try
+                {
+                    executor(Resolve, Reject);
+                }
+                catch (Exception ex)
+                {
+                    Reject(ex);
+                }
+            });
+        }
+
+        private void Resolve(TResult value)
+        {
+            if (State != PromiseState.Pending) return;
+            _resolvedValue = value;
+            State = PromiseState.Resolved;
+            _isResolved = true; // Signal that we are done
+        }
+
+        private void Reject(Exception ex)
+        {
+            if (State != PromiseState.Pending) return;
+            _rejectedException = ex;
+            State = PromiseState.Rejected;
+            _isRejected = true; // Signal that we are done
+        }
+
+        public IPromise<TResult> Then(Action<TResult> onResolved)
+        {
+            _onResolvedCallback = onResolved;
+            return this;
+        }
+
+        public IPromise<TResult> Catch(Action<Exception> onRejected)
+        {
+            _onRejectedCallback = onRejected;
+            return this;
+        }
+
+        public void Finally(Action onFinally)
+        {
+            _onFinallyCallback = onFinally;
+        }
+
+        /// <summary>
+        /// Called by the PromiseManager from the main thread to execute callbacks.
+        /// </summary>
+        /// <returns>True if the promise is settled, otherwise false.</returns>
+        public bool Tick()
+        {
+            if (_isResolved)
+            {
+                try
+                {
+                    _onResolvedCallback?.Invoke(_resolvedValue);
+                }
+                finally
+                {
+                    _onFinallyCallback?.Invoke();
+                }
+                return true;
+            }
+
+            if (_isRejected)
+            {
+                 try
+                 {
+                     _onRejectedCallback?.Invoke(_rejectedException);
+                 }
+                 finally
+                 {
+                     _onFinallyCallback?.Invoke();
+                 }
+                 return true;
+            }
+            
+            return false;
+        }
+    }
+}

+ 3 - 0
Assets/Arbitrator/Editor/Promises/Promise.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e65060235b724a4e924932493ddd5cda
+timeCreated: 1750142676

+ 51 - 0
Assets/Arbitrator/Editor/Promises/PromiseManager.cs

@@ -0,0 +1,51 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// A static manager that hooks into the Unity Editor's update loop to process
+// promises, ensuring their callbacks are executed on the main thread.
+
+using UnityEditor;
+using System.Collections.Generic;
+
+namespace Terra.Arbitrator.Promises
+{
+    [InitializeOnLoad] // Ensures the static constructor is called when the editor loads.
+    public static class PromiseManager
+    {
+        private static readonly List<ITrackablePromise> PendingPromises = new();
+
+        static PromiseManager()
+        {
+            EditorApplication.update -= Update; // Ensure we don't get duplicate subscriptions on recompile
+            EditorApplication.update += Update;
+        }
+
+        private static void Update()
+        {
+            // Lock to prevent modification while iterating, in case a callback adds another promise.
+            lock (PendingPromises)
+            {
+                // Iterate backwards so we can safely remove items.
+                for (var i = PendingPromises.Count - 1; i >= 0; i--)
+                {
+                    // The Tick() method on the promise will return true when it's finished.
+                    // We use dynamic here to call Tick() without knowing the generic type of the promise.
+                    if (PendingPromises[i].Tick())
+                    {
+                        PendingPromises.RemoveAt(i);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Registers a new promise to be ticked by the manager.
+        /// </summary>
+        public static void Track(ITrackablePromise promise)
+        {
+            lock (PendingPromises)
+            {
+                PendingPromises.Add(promise);
+            }
+        }
+    }
+}

+ 3 - 0
Assets/Arbitrator/Editor/Promises/PromiseManager.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d5aa63f5ad0b401d82cb35040d772777
+timeCreated: 1750142696