Преглед изворни кода

Sujith :) ->
1. Attempted to make git conflict work.

sujith пре 1 месец
родитељ
комит
2c9abdfeaf

+ 216 - 65
Assets/Arbitrator/Editor/GUI/ArbitratorWindow.cs

@@ -13,8 +13,8 @@
 using System.Linq;
 using UnityEngine;
 using UnityEditor;
-using LibGit2Sharp;
 using Terra.Arbitrator.Services;
+using Terra.Arbitrator.Settings;
 using System.Collections.Generic;
 
 namespace Terra.Arbitrator.GUI
@@ -31,11 +31,11 @@ namespace Terra.Arbitrator.GUI
         private GUIStyle _evenRowStyle;
         private bool _stylesInitialized;
 
-        [MenuItem("Terra/Changes")]
+        [MenuItem("Version Control/Better Git")]
         public static void ShowWindow()
         {
             var window = GetWindow<ArbitratorWindow>();
-            window.titleContent = new GUIContent("Changes", EditorGUIUtility.IconContent("d_UnityEditor.VersionControl").image);
+            window.titleContent = new GUIContent("Better Git", EditorGUIUtility.IconContent("d_UnityEditor.VersionControl").image);
         }
         
         /// <summary>
@@ -44,7 +44,18 @@ namespace Terra.Arbitrator.GUI
         /// </summary>
         private void OnEnable()
         {
-            HandleCompare();
+            // Check if we just finished a reset operation and need to show a message.
+            if (SessionState.GetBool(BetterGitStatePersistence.ResetQueueKey, false))
+            {
+                SessionState.EraseString(BetterGitStatePersistence.ResetQueueKey);
+                _infoMessage = "Multi-file reset complete. Pulling again to confirm...";
+                // After a successful reset, we should automatically try to pull again.
+                HandlePull();
+            }
+            else
+            {
+                HandleCompare();
+            }
         }
         
         /// <summary>
@@ -117,8 +128,13 @@ namespace Terra.Arbitrator.GUI
             {
                 HandleCompare();
             }
+
+            if (GUILayout.Button(new GUIContent("Pull", EditorGUIUtility.IconContent("CollabPull").image, "Fetch and merge changes from the remote."), EditorStyles.toolbarButton, GUILayout.Width(80)))
+            {
+                HandlePull();
+            }
             
-            if (_changes != null && _changes.Count > 0)
+            if (_changes is { Count: > 0 })
             {
                 if (GUILayout.Button("Select All", EditorStyles.toolbarButton, GUILayout.Width(80)))
                 {
@@ -162,21 +178,130 @@ namespace Terra.Arbitrator.GUI
             _loadingMessage = "Comparing with remote repository...";
             ClearMessages();
             _changes = null;
+            
+            // First, unstage any files if it's safe to do so.
+            GitService.UnstageAllFilesIfSafe()
+                .Then(wasUnstaged =>
+                {
+                    if(wasUnstaged)
+                    {
+                        _infoMessage = "Found and unstaged files for review.";
+                    }
+                    
+                    GitService.CompareLocalToRemote()
+                        .Then(result =>
+                        {
+                            _changes = result;
+                            if (string.IsNullOrEmpty(_infoMessage) && (_changes == null || _changes.Count == 0))
+                            {
+                                _infoMessage = "You are up-to-date! No local changes detected.";
+                            }
+                        })
+                        .Catch(ex =>
+                        {
+                            _errorMessage = $"Comparison Failed: {ex.Message}";
+                        })
+                        .Finally(() =>
+                        {
+                            // This finally block only runs after the *inner* promise is done.
+                            _isLoading = false;
+                            Repaint();
+                        });
+                })
+                .Catch(ex =>
+                {
+                    // This catch block handles errors from the Unstage operation.
+                    _errorMessage = $"Unstaging Failed: {ex.Message}";
+                    _isLoading = false;
+                    Repaint();
+                });
+        }
+        
+        /// <summary>
+        /// Handles the logic for the Pull button.
+        /// </summary>
+        private void HandlePull()
+        {
+            if (_isLoading) return;
+            _isLoading = true;
+            _loadingMessage = "Analyzing for conflicts...";
+            ClearMessages();
+            Repaint();
 
-            GitService.CompareLocalToRemote()
-                .Then(result => {
-                    _changes = result;
-                    if (_changes.Count == 0)
+            GitService.AnalyzePullConflicts()
+                .Then(analysisResult =>
+                {
+                    if (analysisResult.HasConflicts)
+                    {
+                        // Conflicts found! Open the resolution window.
+                        _isLoading = false; // Stop loading while modal is open
+                        Repaint();
+                        ConflictResolutionWindow.ShowWindow(analysisResult.ConflictingFiles, filesToActOn =>
+                        {
+                            if (filesToActOn == null) return; // User cancelled
+                            if (filesToActOn.Any()) HandleResetMultipleFiles(filesToActOn);
+                            else HandleForcePull(); // User chose "Pull Anyway" (empty list signal)
+                        });
+                    }
+                    else
                     {
-                        _infoMessage = "You are up-to-date! No local changes detected.";
+                        // No conflicts, proceed with a safe pull.
+                        _loadingMessage = "No conflicts found. Pulling changes...";
+                        Repaint();
+                        GitService.PerformSafePull()
+                            .Then(successMessage =>
+                            {
+                                _infoMessage = successMessage;
+                                EditorUtility.DisplayDialog("Pull Complete", successMessage, "OK");
+                                HandleCompare(); // Refresh view after successful pull
+                            })
+                            .Catch(ex =>
+                            {
+                                _errorMessage = $"Pull Failed: {ex.Message}";
+                                _isLoading = false;
+                                Repaint();
+                            });
                     }
                 })
-                .Catch(ex => {
-                    _errorMessage = $"Comparison Failed: {ex.Message}";
+                .Catch(ex =>
+                {
+                    _errorMessage = $"Pull Analysis Failed: {ex.Message}";
+                    _isLoading = false;
+                    Repaint();
+                });
+        }
+        
+        private void HandleResetMultipleFiles(List<string> filePaths)
+        {
+            _infoMessage = $"Starting reset for {filePaths.Count} file(s)... This may trigger script compilation.";
+            Repaint();
+            SessionState.SetString("BetterGit.ResetQueue", string.Join(";", filePaths));
+            EditorApplication.delayCall += BetterGitStatePersistence.ContinueInterruptedReset;
+        }
+        
+        private void HandleForcePull()
+        {
+            _isLoading = true;
+            _loadingMessage = "Attempting to pull and create conflicts...";
+            ClearMessages();
+            Repaint();
+            EditorApplication.LockReloadAssemblies();
+            GitService.ForcePull()
+                .Then(_ =>
+                {
+                    _infoMessage = "Pull resulted in conflicts. Please resolve them below.";
+                    HandleCompare();
                 })
-                .Finally(() => {
+                .Catch(ex =>
+                {
+                    _errorMessage = $"Forced Pull Failed: {ex.Message}";
                     _isLoading = false;
-                    Repaint(); // Redraw the window with the new state
+                    Repaint();
+                })
+                .Finally(() =>
+                {
+                    EditorApplication.UnlockReloadAssemblies();
+                    AssetDatabase.Refresh();
                 });
         }
 
@@ -187,8 +312,11 @@ namespace Terra.Arbitrator.GUI
             ClearMessages();
 
             var selectedFiles = _changes.Where(c => c.IsSelectedForCommit).ToList();
+            
+            var username = BetterGitSettings.Username;
+            var email = BetterGitSettings.Email;
 
-            GitService.CommitAndPush(selectedFiles, _commitMessage)
+            GitService.CommitAndPush(selectedFiles, _commitMessage, username, email)
                 .Then(successMessage => {
                     _infoMessage = successMessage;
                     _commitMessage = ""; // Clear message on success
@@ -208,51 +336,58 @@ namespace Terra.Arbitrator.GUI
         private void HandleResetFile(GitChange change)
         {
             if (_isLoading) return;
+            var userConfirmed = EditorUtility.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;
             _isLoading = true;
-            _loadingMessage = $"Generating diff for {change.FilePath}...";
+            _loadingMessage = $"Resetting {change.FilePath}...";
             ClearMessages();
             Repaint();
-
-            // Step 1: Get the diff content for the file.
-            GitService.GetFileDiff(change)
-                .Then(diffContent =>
+            GitService.ResetFileChanges(change)
+                .Then(successMessage => {
+                    _infoMessage = successMessage;
+                    HandleCompare();
+                })
+                .Catch(ex => {
+                    _errorMessage = $"Reset Failed: {ex.Message}";
+                    _isLoading = false;
+                    Repaint();
+                });
+        }
+        
+        private void HandleDiffFile(GitChange change)
+        {
+            if (_isLoading) return;
+            _isLoading = true;
+            _loadingMessage = $"Launching diff for {change.FilePath}...";
+            ClearMessages();
+            Repaint();
+            GitService.LaunchExternalDiff(change)
+                .Catch(ex => _errorMessage = ex.Message)
+                .Finally(() => { _isLoading = false; HandleCompare(); });
+        }
+        
+        private void HandleResolveConflict(GitChange change)
+        {
+            if (_isLoading) return;
+            _isLoading = true;
+            _loadingMessage = $"Opening merge tool for {change.FilePath}...";
+            ClearMessages();
+            Repaint();
+            
+            GitService.LaunchMergeTool(change)
+                .Then(successMessage =>
                 {
-                    // 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();
-                            });
-                    });
+                    _infoMessage = successMessage;
                 })
                 .Catch(ex =>
                 {
-                    // This runs if getting the diff itself failed.
-                    _errorMessage = $"Could not generate diff: {ex.Message}";
-                    _isLoading = false;
-                    Repaint();
+                    _errorMessage = $"Resolve Failed: {ex.Message}";
                 })
-                .Finally(HandleCompare);
+                .Finally(() =>
+                {
+                    _isLoading = false;
+                    HandleCompare(); // Always refresh to show the new state
+                });
         }
 
         /// <summary>
@@ -262,7 +397,9 @@ namespace Terra.Arbitrator.GUI
         {
             // --- Draw Header ---
             EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
-            EditorGUILayout.LabelField("Commit", GUILayout.Width(45));
+            var isConflictMode = _changes.Any(c => c.Status == LibGit2Sharp.ChangeKind.Conflicted);
+            if (!isConflictMode) EditorGUILayout.LabelField("Commit", GUILayout.Width(45));
+            else GUILayout.Space(45); // Keep layout consistent
             EditorGUILayout.LabelField("Status", GUILayout.Width(50));
             EditorGUILayout.LabelField("File Path");
             EditorGUILayout.LabelField("Actions", GUILayout.Width(55));
@@ -286,22 +423,23 @@ namespace Terra.Arbitrator.GUI
 
                 switch (change.Status)
                 {
-                    case ChangeKind.Added: 
+                    case LibGit2Sharp.ChangeKind.Added: 
                         status = "[+]"; statusColor = Color.green; filePathDisplay = change.FilePath; break;
-                    case ChangeKind.Deleted: 
+                    case LibGit2Sharp.ChangeKind.Deleted: 
                         status = "[-]"; statusColor = Color.red; filePathDisplay = change.FilePath; break;
-                    case ChangeKind.Modified: 
+                    case LibGit2Sharp.ChangeKind.Modified: 
                         status = "[M]"; statusColor = new Color(1.0f, 0.6f, 0.0f); filePathDisplay = change.FilePath; break;
-                    case ChangeKind.Renamed: 
+                    case LibGit2Sharp.ChangeKind.Renamed: 
                         status = "[R]"; statusColor = new Color(0.6f, 0.6f, 1.0f);
                         filePathDisplay = $"{change.OldFilePath} -> {change.FilePath}"; break;
-                    case ChangeKind.Unmodified:
-                    case ChangeKind.Copied:
-                    case ChangeKind.Ignored:
-                    case ChangeKind.Untracked:
-                    case ChangeKind.TypeChanged:
-                    case ChangeKind.Unreadable:
-                    case ChangeKind.Conflicted:
+                    case LibGit2Sharp.ChangeKind.Conflicted:
+                        status = "[C]"; statusColor = Color.magenta; filePathDisplay = change.FilePath; break;
+                    case LibGit2Sharp.ChangeKind.Unmodified:
+                    case LibGit2Sharp.ChangeKind.Copied:
+                    case LibGit2Sharp.ChangeKind.Ignored:
+                    case LibGit2Sharp.ChangeKind.Untracked:
+                    case LibGit2Sharp.ChangeKind.TypeChanged:
+                    case LibGit2Sharp.ChangeKind.Unreadable:
                     default: 
                         status = "[?]"; statusColor = Color.white; filePathDisplay = change.FilePath; break;
                 }
@@ -314,6 +452,14 @@ namespace Terra.Arbitrator.GUI
                 EditorGUILayout.LabelField(new GUIContent(filePathDisplay, filePathDisplay));
 
                 EditorGUI.BeginDisabledGroup(_isLoading);
+                if (status == "[C]")
+                {
+                    if (GUILayout.Button("Resolve", GUILayout.Width(70))) { EditorApplication.delayCall += () => HandleResolveConflict(change); }
+                }
+                else
+                {
+                    if (GUILayout.Button("Diff", GUILayout.Width(45))) EditorApplication.delayCall += () => HandleDiffFile(change);
+                }
                 if (GUILayout.Button(new GUIContent("Reset", "Revert changes for this file"), GUILayout.Width(55)))
                 {
                     EditorApplication.delayCall += () => HandleResetFile(change);
@@ -328,6 +474,12 @@ namespace Terra.Arbitrator.GUI
         private void DrawCommitSection()
         {
             EditorGUILayout.Space(10);
+            var isConflictMode = _changes.Any(c => c.Status == LibGit2Sharp.ChangeKind.Conflicted);
+            if (isConflictMode)
+            {
+                EditorGUILayout.HelpBox("You must resolve all conflicts before you can commit.", MessageType.Warning);
+                return;
+            }
             EditorGUILayout.LabelField("Commit & Push", EditorStyles.boldLabel);
             _commitMessage = EditorGUILayout.TextArea(_commitMessage, GUILayout.Height(60), GUILayout.ExpandWidth(true));
 
@@ -336,7 +488,6 @@ namespace Terra.Arbitrator.GUI
             EditorGUI.BeginDisabledGroup(isPushDisabled);
             if (GUILayout.Button("Commit & Push Selected Files", GUILayout.Height(40)))
             {
-                Debug.Log("Commit & Push Selected Files");
                 HandleCommitAndPush();
             }
             EditorGUI.EndDisabledGroup();

+ 47 - 2
Assets/Arbitrator/Editor/GUI/BetterGitSettingsProvider.cs

@@ -1,7 +1,52 @@
+using UnityEditor;
+using UnityEngine;
+using Terra.Arbitrator.Settings;
+
 namespace Terra.Arbitrator.GUI
 {
-    public class BetterGitSettingsProvider
+    internal class BetterGitSettingsProvider : SettingsProvider
     {
-        
+        // The path in the Project Settings window.
+        private const string SettingsPath = "Project/Better Git";
+
+        // Constructor
+        private BetterGitSettingsProvider(string path, SettingsScope scope = SettingsScope.User)
+            : base(path, scope) {}
+
+        /// <summary>
+        /// This static method is what registers our settings provider with Unity.
+        /// </summary>
+        [SettingsProvider]
+        public static SettingsProvider CreateSettingsProvider()
+        {
+            var provider = new BetterGitSettingsProvider(SettingsPath, SettingsScope.Project);
+            return provider;
+        }
+
+        /// <summary>
+        /// This is where we draw the UI for our settings page.
+        /// </summary>
+        public override void OnGUI(string searchContext)
+        {
+            // Load the current settings directly within OnGUI.
+            var username = BetterGitSettings.Username;
+            var email = BetterGitSettings.Email;
+            
+            EditorGUILayout.Space();
+
+            EditorGUILayout.LabelField("Git Credentials", EditorStyles.boldLabel);
+            EditorGUILayout.HelpBox("These credentials will be used for Git operations like Push. They are saved per-user and shared across your projects. Changes are saved automatically.", MessageType.Info);
+            
+            EditorGUILayout.Space();
+
+            
+            EditorGUI.BeginChangeCheck();
+            username = EditorGUILayout.TextField("Username", username);
+            email = EditorGUILayout.TextField("Email Address", email);
+            if (!EditorGUI.EndChangeCheck()) return;
+            
+            BetterGitSettings.Username = username;
+            BetterGitSettings.Email = email;
+        }
     }
 }

+ 3 - 0
Assets/Arbitrator/Editor/GUI/ConflictResolutionWindow.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5922114068204ccca987c134c406371a
+timeCreated: 1750677666

+ 1 - 1
Assets/Arbitrator/Editor/Promises/Promise.cs

@@ -10,7 +10,7 @@ namespace Terra.Arbitrator.Promises
 {
     public class Promise<TResult> : IPromise<TResult>, ITrackablePromise
     {
-        public PromiseState State { get; private set; }
+        private PromiseState State { get; set; }
         private TResult _resolvedValue;
         private Exception _rejectedException;
 

+ 73 - 0
Assets/Arbitrator/Editor/Services/BetterGitStatePersistence.cs

@@ -0,0 +1,73 @@
+// Copyright (c) 2025 TerraByte Inc.
+//
+// A static class that persists the state of a multi-file reset operation
+// across script re-compilations, ensuring the process is not interrupted.
+
+using System.Linq;
+using UnityEditor;
+
+namespace Terra.Arbitrator.Services
+{
+    [InitializeOnLoad]
+    public static class BetterGitStatePersistence
+    {
+        internal const string ResetQueueKey = "BetterGit.ResetQueue";
+
+        // This static constructor runs automatically after every script compilation.
+        static BetterGitStatePersistence()
+        {
+            // Use delayCall to ensure this runs after the editor is fully initialized.
+            EditorApplication.delayCall += ContinueInterruptedReset;
+        }
+
+        public static void ContinueInterruptedReset()
+        {
+            while (true)
+            {
+                var queuedFiles = SessionState.GetString(ResetQueueKey, "");
+                if (string.IsNullOrEmpty(queuedFiles))
+                {
+                    return; // Nothing to do.
+                }
+
+                var fileList = queuedFiles.Split(';').ToList();
+                if (!fileList.Any())
+                {
+                    SessionState.EraseString(ResetQueueKey);
+                    return;
+                }
+
+                // Get the next file to process
+                var fileToReset = fileList.First();
+                fileList.RemoveAt(0);
+
+                // Update the session state with the remaining files BEFORE starting the operation.
+                if (fileList.Any())
+                {
+                    SessionState.SetString(ResetQueueKey, string.Join(";", fileList));
+                }
+                else
+                {
+                    // Last file was processed, clear the key.
+                    SessionState.EraseString(ResetQueueKey);
+                }
+
+                // Create a GitChange object for the file to be reset.
+                // This is a small, synchronous operation to get the file's current status.
+                var change = GitService.GetChangeForFile(fileToReset);
+                if (change == null)
+                {
+                    // The File might have already been reset or state is unusual.
+                    // We'll log it and attempt to continue.
+                    UnityEngine.Debug.LogWarning($"Could not find status for {fileToReset}, skipping reset. It may have already been processed.");
+                    continue;
+                }
+
+                // Trigger the reset. After this, Unity will recompile if it was a script,
+                // and this whole static constructor will run again.
+                GitService.ResetFileChanges(change);
+                break;
+            }
+        }
+    }
+}

+ 3 - 0
Assets/Arbitrator/Editor/Services/BetterGitStatePersistence.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: deefd0d04b804e368c5bd45a1802d698
+timeCreated: 1750678265

+ 576 - 55
Assets/Arbitrator/Editor/Services/GitService.cs

@@ -4,11 +4,19 @@
 // repository. All public methods are asynchronous and return a promise.
 
 using System;
+using CliWrap;
 using System.IO;
-using LibGit2Sharp;
+using System.Linq;
 using UnityEngine;
+using System.Text;
+using LibGit2Sharp;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Terra.Arbitrator.Settings;
 using Terra.Arbitrator.Promises;
 using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using Debug = UnityEngine.Debug;
 
 namespace Terra.Arbitrator.Services
 {
@@ -17,15 +25,15 @@ namespace Terra.Arbitrator.Services
         // Public method that returns the promise
         public static IPromise<List<GitChange>> CompareLocalToRemote()
         {
-            return new Promise<List<GitChange>>(CompareExecutor);
+            return new Promise<List<GitChange>>(GetLocalStatusExecutor);
         }
         
         // Public method that returns the promise
-        public static IPromise<string> CommitAndPush(List<GitChange> changesToCommit, string commitMessage)
+        public static IPromise<string> CommitAndPush(List<GitChange> changesToCommit, string commitMessage, string username, string email)
         {
             // Use a lambda here to pass arguments to the executor method
             return new Promise<string>((resolve, reject) => 
-                CommitAndPushExecutor(resolve, reject, changesToCommit, commitMessage));
+                CommitAndPushExecutor(resolve, reject, changesToCommit, commitMessage, username, email));
         }
         
         // Creates a promise to revert a single file to its HEAD revision.
@@ -34,51 +42,315 @@ namespace Terra.Arbitrator.Services
             return new Promise<string>((resolve, reject) => 
                 ResetFileExecutor(resolve, reject, changeToReset));
         }
+
+        // Creates a promise to launch an external diff tool (VS Code).
+        public static IPromise<string> LaunchExternalDiff(GitChange change)
+        {
+            return new Promise<string>((resolve, reject) => LaunchExternalDiffExecutor(resolve, reject, change));
+        }
         
-        // Creates a promise to get the diff patch for a single file against HEAD.
-        public static IPromise<string> GetFileDiff(GitChange change)
+        // Performs a non-destructive analysis of a potential pull.
+        public static IPromise<PullAnalysisResult> AnalyzePullConflicts()
+        {
+            return new Promise<PullAnalysisResult>(FileLevelConflictCheckExecutor);
+        }
+
+        // Performs a pull, assuming analysis has already confirmed it's safe.
+        public static IPromise<string> PerformSafePull()
         {
-            return new Promise<string>((resolve, reject) => GetFileDiffExecutor(resolve, reject, change));
+            return new Promise<string>(SafePullExecutor);
         }
 
+        // Creates a promise to perform a pull that may result in conflicts.
+        public static IPromise<string> ForcePull()
+        {
+            return new Promise<string>(ForcePullExecutor);
+        }
+        
+        // Creates a promise to launch an external merge tool for a conflicted file.
+        public static IPromise<string> LaunchMergeTool(GitChange change)
+        {
+            return new Promise<string>((resolve, reject) => LaunchMergeToolExecutor(resolve, reject, change));
+        }
+        
+        /// <summary>
+        /// Creates a promise to unstage all files if the repository is in a clean, non-conflicted state.
+        /// </summary>
+        public static IPromise<bool> UnstageAllFilesIfSafe()
+        {
+            return new Promise<bool>(UnstageAllFilesIfSafeExecutor);
+        }
+        
         /// <summary>
-        /// The private logic for the CompareLocalToRemote promise.
-        /// This code is executed on a background thread by the Promise constructor.
+        /// Synchronous helper to get a GitChange object for a single file.
+        /// Needed by the static persistence script which cannot easily use promises.
         /// </summary>
-        private static void CompareExecutor(Action<List<GitChange>> resolve, Action<Exception> reject)
+        public static GitChange GetChangeForFile(string filePath)
+        {
+            try
+            {
+                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                using var repo = new Repository(projectRoot);
+                var statusEntry = repo.RetrieveStatus(filePath);
+
+                // Determine ChangeKind from FileStatus
+                return statusEntry switch
+                {
+                    FileStatus.NewInWorkdir or FileStatus.NewInIndex => new GitChange(filePath, null, ChangeKind.Added),
+                    FileStatus.ModifiedInWorkdir or FileStatus.ModifiedInIndex => new GitChange(filePath, null, ChangeKind.Modified),
+                    FileStatus.DeletedFromWorkdir or FileStatus.DeletedFromIndex => new GitChange(filePath, null, ChangeKind.Deleted),
+                    FileStatus.RenamedInWorkdir or FileStatus.RenamedInIndex =>
+                        // Getting the old path from a single status entry is complex,
+                        // for reset purposes, treating it as modified is safe enough.
+                        new GitChange(filePath, null, ChangeKind.Renamed),
+                    _ => null
+                };
+            }
+            catch { return null; } // Suppress errors if repo is in a unique state
+        }
+        
+        /// <summary>
+        /// Gets the status of the local repository, showing unstaged changes.
+        /// This is the equivalent of 'git status'.
+        /// </summary>
+        private static void GetLocalStatusExecutor(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 conflictedPaths = new HashSet<string>(repo.Index.Conflicts.Select(c => c.Ours.Path));
+
+                var statusOptions = new StatusOptions
+                {
+                    IncludeUntracked = true, 
+                    RecurseUntrackedDirs = true,
+                    DetectRenamesInIndex = true,
+                    DetectRenamesInWorkDir = true
+                };
+                foreach (var entry in repo.RetrieveStatus(statusOptions))
+                {
+                    if (conflictedPaths.Contains(entry.FilePath))
+                    {
+                        changes.Add(new GitChange(entry.FilePath, null, ChangeKind.Conflicted));
+                        continue;
+                    }
+                    switch(entry.State)
+                    {
+                        case FileStatus.NewInWorkdir:
+                        case FileStatus.NewInIndex:
+                            changes.Add(new GitChange(entry.FilePath, null, ChangeKind.Added));
+                            break;
+                        
+                        case FileStatus.ModifiedInWorkdir:
+                        case FileStatus.ModifiedInIndex:
+                            changes.Add(new GitChange(entry.FilePath, null, ChangeKind.Modified));
+                            break;
+
+                        case FileStatus.DeletedFromWorkdir:
+                        case FileStatus.DeletedFromIndex:
+                            changes.Add(new GitChange(entry.FilePath, null, ChangeKind.Deleted));
+                            break;
+
+                        case FileStatus.RenamedInWorkdir:
+                        case FileStatus.RenamedInIndex:
+                            var renameDetails = entry.HeadToIndexRenameDetails ?? entry.IndexToWorkDirRenameDetails;
+                            changes.Add(renameDetails != null ? new GitChange(renameDetails.NewFilePath, renameDetails.OldFilePath, ChangeKind.Renamed)
+                                // Fallback for safety, though this path should rarely be hit with correct options.
+                                : new GitChange(entry.FilePath, "Unknown", ChangeKind.Renamed));
+                            break;
+                        
+                        case FileStatus.Nonexistent:
+                        case FileStatus.Unaltered:
+                        case FileStatus.TypeChangeInIndex:
+                        case FileStatus.TypeChangeInWorkdir:
+                        case FileStatus.Unreadable:
+                        case FileStatus.Ignored:
+                        case FileStatus.Conflicted:
+                        default:
+                            break;
+                    }
+                }
+                
+                resolve(changes);
+            }
+            catch (Exception ex)
+            {
+                Debug.LogException(ex);
+                reject(ex);
+            }
+        }
+        
+        private static void FileLevelConflictCheckExecutor(Action<PullAnalysisResult> resolve, Action<Exception> reject)
+        {
+            try
+            {
+                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                using var repo = new Repository(projectRoot);
+                var result = AnalyzePullConflicts(repo).Result; // .Result is safe here as it's wrapped in a promise
+                resolve(result);
+            }
+            catch (Exception ex)
+            {
+                reject(ex);
+            }
+        }
+
+        private static Task<PullAnalysisResult> AnalyzePullConflicts(Repository repo)
+        {
+            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 { CertificateCheck = (certificate, valid, host) => true }, null);
+            var localBranch = repo.Head;
+            var remoteBranch = repo.Head.TrackedBranch;
+            if (remoteBranch == null) throw new Exception("Current branch is not tracking a remote branch.");
+            var mergeBase = repo.ObjectDatabase.FindMergeBase(localBranch.Tip, remoteBranch.Tip);
+            if (mergeBase == null) throw new Exception("Could not find a common ancestor.");
+            
+            var theirChanges = new HashSet<string>();
+            var remoteDiff = repo.Diff.Compare<TreeChanges>(mergeBase.Tree, remoteBranch.Tip.Tree);
+            foreach (var change in remoteDiff) theirChanges.Add(change.Path);
+            
+            var ourChanges = new HashSet<string>();
+            var localCommitDiff = repo.Diff.Compare<TreeChanges>(mergeBase.Tree, localBranch.Tip.Tree);
+            foreach (var change in localCommitDiff) ourChanges.Add(change.Path);
+            var localStatus = repo.RetrieveStatus();
+            foreach (var statusEntry in localStatus) ourChanges.Add(statusEntry.FilePath);
+            
+            var conflictingFiles = ourChanges.Where(theirChanges.Contains).ToList();
 
+            return Task.FromResult(new PullAnalysisResult(conflictingFiles));
+        }
+
+        private static async void PullExecutor(Action<string> resolve, Action<Exception> reject)
+        {
+            try
+            {
+                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                using var repo = new Repository(projectRoot);
+                
+                // 1. Analyze for conflicts first.
+                var analysisResult = await AnalyzePullConflicts(repo);
+                if (analysisResult.HasConflicts)
+                {
+                    var conflictFiles = string.Join("\n - ", analysisResult.ConflictingFiles);
+                    throw new Exception($"Potential conflicts detected in the following files. Please reset or commit your local changes first:\n - {conflictFiles}");
+                }
+                
+                // 2. If analysis passes, proceed with the pull.
                 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 signature = new Signature("Better Git Tool", "bettergit@example.com", DateTimeOffset.Now);
+                var pullOptions = new PullOptions { FetchOptions = new FetchOptions { CertificateCheck = (certificate, valid, host) => true } };
                 
-                var remoteBranch = repo.Head.TrackedBranch;
-                if (remoteBranch == null || !remoteBranch.IsRemote)
+                var mergeResult = Commands.Pull(repo, signature, pullOptions);
+
+                resolve(mergeResult.Status == MergeStatus.UpToDate
+                    ? "Already up-to-date."
+                    : $"Pull successful. Status: {mergeResult.Status}");
+            }
+            catch (Exception ex)
+            {
+                reject(ex);
+            }
+        }
+        
+        private static void SafePullExecutor(Action<string> resolve, Action<Exception> reject)
+        {
+            try
+            {
+                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                using var repo = new Repository(projectRoot);
+                var signature = new Signature("Better Git Tool", "bettergit@example.com", DateTimeOffset.Now);
+                var pullOptions = new PullOptions { FetchOptions = new FetchOptions { CertificateCheck = (certificate, valid, host) => true } };
+                var mergeResult = Commands.Pull(repo, signature, pullOptions);
+                resolve(mergeResult.Status == MergeStatus.UpToDate ? "Already up-to-date." : $"Pull successful. Status: {mergeResult.Status}");
+            }
+            catch (Exception ex)
+            {
+                reject(ex);
+            }
+        }
+        
+        private static async void ForcePullExecutor(Action<string> resolve, Action<Exception> reject)
+        {
+            var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+            var hasLocalChanges = false;
+            var log = new StringBuilder();
+            var hasStashed = false;
+
+            try
+            {
+                using (var repo = new Repository(projectRoot))
                 {
-                    throw new Exception($"Current branch '{repo.Head.FriendlyName}' is not tracking a remote branch.");
+                    if (repo.RetrieveStatus().IsDirty) hasLocalChanges = true;
                 }
+                log.AppendLine($"Step 0: Has local changes? {hasLocalChanges}");
 
-                var diff = repo.Diff.Compare<TreeChanges>(remoteBranch.Tip.Tree, DiffTargets.Index | DiffTargets.WorkingDirectory);
+                if (hasLocalChanges)
+                {
+                    await ExecuteGitCommandAsync("stash push -u -m \"BetterGit-WIP-Pull\"", projectRoot, log, 0, 141);
+                    hasStashed = true;
+                }
+                
+                // Use the simpler, more robust pull command.
+                await ExecuteGitCommandAsync("pull --no-rebase", projectRoot, log, 0, 1, 141);
 
-                foreach (var entry in diff)
+                if (hasStashed)
                 {
-                    if (entry.Status is ChangeKind.Added or ChangeKind.Deleted or ChangeKind.Modified or ChangeKind.Renamed)
-                    {
-                        changes.Add(new GitChange(entry.Path, entry.OldPath, entry.Status));
-                    }
+                    await ExecuteGitCommandAsync("stash pop", projectRoot, log, 0, 1, 141);
+                    hasStashed = false;
                 }
                 
-                resolve(changes); // Success
+                resolve(log.ToString());
             }
             catch (Exception ex)
             {
-                reject(ex); // Failure
+                if (hasStashed)
+                {
+                    try
+                    {
+                        await ExecuteGitCommandAsync("stash pop", projectRoot, log, 0, 1, 141);
+                    }
+                    catch (Exception exception)
+                    {
+                        log.AppendLine($"Fatal Error: {exception.Message}");
+                    }
+                }
+                log.AppendLine("\n--- FAILED ---");
+                log.AppendLine(ex.ToString());
+                reject(new Exception(log.ToString()));
+            }
+        }
+        
+        /// <summary>
+        /// A reusable helper method to execute Git commands with detailed logging and exit code validation.
+        /// </summary>
+        private static async Task ExecuteGitCommandAsync(string args, string workingDir, StringBuilder log, params int[] acceptableExitCodes)
+        {
+            var stdOutBuffer = new StringBuilder();
+            var stdErrBuffer = new StringBuilder();
+            log?.AppendLine($"\n--- Executing: git {args} ---");
+
+            var command = Cli.Wrap(FindGitExecutable())
+                              .WithArguments(args)
+                              .WithWorkingDirectory(workingDir)
+                              .WithValidation(CommandResultValidation.None) // We handle validation manually
+                          | (PipeTarget.ToDelegate(x => stdOutBuffer.Append(x)), PipeTarget.ToDelegate(x => stdErrBuffer.Append(x)));
+
+            var result = await command.ExecuteAsync();
+
+            log?.AppendLine($"Exit Code: {result.ExitCode}");
+            if(stdOutBuffer.Length > 0) log?.AppendLine($"StdOut: {stdOutBuffer}");
+            if(stdErrBuffer.Length > 0) log?.AppendLine($"StdErr: {stdErrBuffer}");
+
+            // If the exit code is not in our list of acceptable codes, it's an error.
+            if (!acceptableExitCodes.Contains(result.ExitCode))
+            {
+                throw new Exception($"Command 'git {args}' failed with unexpected exit code {result.ExitCode}.");
             }
         }
 
@@ -86,43 +358,75 @@ namespace Terra.Arbitrator.Services
         /// 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)
+        private static async void CommitAndPushExecutor(Action<string> resolve, Action<Exception> reject, List<GitChange> changesToCommit, string commitMessage, string username, string email)
         {
             try
             {
+                if (string.IsNullOrWhiteSpace(email))
+                {
+                    throw new Exception("Author email is missing. Please set your email address in Project Settings > Better Git.");
+                }
+
                 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.");
+                if (projectRoot == null)
+                {
+                    throw new Exception("Could not find project root.");
+                }
                 
-                Commands.Fetch(repo, remote.Name, Array.Empty<string>(), new FetchOptions(), "Arbitrator pre-push fetch");
-
-                var trackingDetails = repo.Head.TrackingDetails;
-                if (trackingDetails.BehindBy > 0)
+                using (var repo = new Repository(projectRoot))
                 {
-                    throw new Exception($"Push aborted. There are {trackingDetails.BehindBy.Value} incoming changes on the remote. Please pull first.");
+                    var remote = repo.Network.Remotes["origin"];
+                    if (remote == null) throw new Exception("No remote named 'origin' found.");
+                    
+                    var fetchOptions = new FetchOptions { CertificateCheck = (_, _, _) => true };
+                    Commands.Fetch(repo, remote.Name, Array.Empty<string>(), 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.");
+                    }
+
+                    var pathsToStage = new List<string>();
+                    foreach (var change in changesToCommit)
+                    {
+                        if (change.Status == ChangeKind.Deleted) Commands.Remove(repo, change.FilePath);
+                        else pathsToStage.Add(change.FilePath);
+                    }
+                    if (pathsToStage.Any()) Commands.Stage(repo, pathsToStage);
+
+                    var status = repo.RetrieveStatus();
+                    if (!status.IsDirty) throw new Exception("No effective changes were staged to commit.");
+                    
+                    var author = new Signature(username, email, DateTimeOffset.Now);
+                    repo.Commit(commitMessage, author, author);
                 }
 
-                foreach (var change in changesToCommit)
+                var gitExecutable = FindGitExecutable();
+                
+                var result = await Cli.Wrap(gitExecutable)
+                    .WithArguments("push")
+                    .WithWorkingDirectory(projectRoot)
+                    .WithValidation(CommandResultValidation.None) // Don't throw exceptions automatically
+                    .ExecuteAsync();
+                
+                // Exit code 141 means the command was successful but was terminated by a SIGPIPE signal. This is safe to ignore on macOS/Linux.
+                if (result.ExitCode != 0 && result.ExitCode != 141)
                 {
-                    if (change.Status == ChangeKind.Deleted) Commands.Remove(repo, change.FilePath);
-                    else Commands.Stage(repo, change.FilePath);
+                    throw new Exception($"Push Failed. Error code: {result.ExitCode}");
                 }
 
-                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
+                resolve("Successfully committed and pushed changes!");
             }
             catch (Exception ex)
             {
-                reject(ex); // Failure
+                Debug.LogException(ex);
+                var errorMessage = ex.InnerException?.Message ?? ex.Message;
+                reject(new Exception(errorMessage));
             }
         }
-        
+
         /// <summary>
         /// The private logic for resetting a file's changes.
         /// This is executed on a background thread.
@@ -178,34 +482,251 @@ namespace Terra.Arbitrator.Services
             }
         }
         
-        private static void GetFileDiffExecutor(Action<string> resolve, Action<Exception> reject, GitChange change)
+        private static async void LaunchExternalDiffExecutor(Action<string> resolve, Action<Exception> reject, GitChange change)
         {
+            string fileAPath = null; // Before
+            string fileBPath = null; // After
+            
             try
             {
                 var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                if (projectRoot == null)
+                {
+                    reject(new Exception("Could not find project root."));
+                    return;
+                }
+                
                 using var repo = new Repository(projectRoot);
-
-                // Compare the HEAD tree with the working directory.
-                var patch = repo.Diff.Compare<Patch>(repo.Head.Tip.Tree, DiffTargets.WorkingDirectory);
                 
-                // Find the specific change in the complete patch by its path.
-                // For renames, the 'Path' property holds the new path, which is what we need to look up.
-                var patchEntry = patch[change.FilePath];
+                // Get the content of a file from a specific commit.
+                string GetFileContentFromHead(string path)
+                {
+                    var blob = repo.Head.Tip[path]?.Target as Blob;
+                    return blob?.GetContentText() ?? "";
+                }
 
-                if (patchEntry == null)
+                // Create a temporary file with the correct extension for syntax highlighting.
+                string CreateTempFile(string originalPath, string content)
                 {
-                    // This might happen if the change is in the index but not the working dir.
-                    // Fallback to checking the index as well.
-                    patch = repo.Diff.Compare<Patch>(repo.Head.Tip.Tree, DiffTargets.Index);
-                    patchEntry = patch[change.FilePath];
+                    var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + Path.GetExtension(originalPath));
+                    File.WriteAllText(tempPath, content);
+                    return tempPath;
                 }
 
-                resolve(patchEntry?.Patch ?? $"No textual changes detected for {change.FilePath}. Status: {change.Status}");
+                switch (change.Status)
+                {
+                    case ChangeKind.Added:
+                        fileAPath = CreateTempFile(change.FilePath, ""); // Empty "before" file
+                        fileBPath = Path.Combine(projectRoot, change.FilePath);
+                        break;
+                    
+                    case ChangeKind.Deleted:
+                        fileAPath = CreateTempFile(change.FilePath, GetFileContentFromHead(change.FilePath));
+                        fileBPath = CreateTempFile(change.FilePath, ""); // Empty "after" file
+                        break;
+
+                    case ChangeKind.Renamed:
+                         fileAPath = CreateTempFile(change.OldFilePath, GetFileContentFromHead(change.OldFilePath));
+                         fileBPath = Path.Combine(projectRoot, change.FilePath);
+                        break;
+
+                    case ChangeKind.Unmodified:
+                    case ChangeKind.Modified:
+                    case ChangeKind.Copied:
+                    case ChangeKind.Ignored:
+                    case ChangeKind.Untracked:
+                    case ChangeKind.TypeChanged:
+                    case ChangeKind.Unreadable:
+                    case ChangeKind.Conflicted:
+                    default: // Modified
+                        fileAPath = CreateTempFile(change.FilePath, GetFileContentFromHead(change.FilePath));
+                        fileBPath = Path.Combine(projectRoot, change.FilePath);
+                        break;
+                }
+                
+                var vsCodeExecutable = FindVsCodeExecutable();
+                
+                await Cli.Wrap(vsCodeExecutable)
+                    .WithArguments(args => args
+                        .Add("--diff")
+                        .Add(fileAPath)
+                        .Add(fileBPath))
+                    .ExecuteAsync();
+
+                resolve("Launched external diff tool.");
+            }
+            catch(Win32Exception ex)
+            {
+                // This specific exception is thrown if the executable is not found.
+                reject(new Exception("Could not launch VS Code. Ensure it is installed and the 'code' command is available in your system's PATH.", ex));
             }
             catch(Exception ex)
             {
                 reject(ex);
             }
+            finally
+            {
+                // We only delete files we created (i.e., not the actual project file).
+                try
+                {
+                    if (fileAPath != null && fileAPath.Contains(Path.GetTempPath()) && File.Exists(fileAPath))
+                    {
+                        File.Delete(fileAPath);
+                    }
+                    if (fileBPath != null && fileBPath.Contains(Path.GetTempPath()) && File.Exists(fileBPath))
+                    {
+                        File.Delete(fileBPath);
+                    }
+                }
+                catch(Exception cleanupEx)
+                {
+                    // Log cleanup error but don't throw, as the primary exception (if any) is more important.
+                    Debug.LogError($"Failed to clean up temporary diff files: {cleanupEx.Message}");
+                }
+            }
         }
+        
+        /// <summary>
+        /// The private logic for launching a merge tool for a conflicted file.
+        /// </summary>
+        private static async void LaunchMergeToolExecutor(Action<string> resolve, Action<Exception> reject, GitChange change)
+        {
+            var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+            if (projectRoot == null)
+            {
+                reject(new Exception("Could not find project root."));
+                return;
+            }
+            if (change.FilePath == null)
+            {
+                reject(new Exception("Could not find file path."));
+                return;
+            }
+
+            try
+            {
+                var fileExtension = Path.GetExtension(change.FilePath)?.ToLower();
+                if (fileExtension is ".prefab" or ".unity")
+                {
+                    reject(new Exception("Cannot resolve conflicts for binary files (.prefab, .unity) with VS Code. Please use an external merge tool."));
+                    return;
+                }
+
+                // Launch VS Code and wait for it to close.
+                var vsCodeExecutable = FindVsCodeExecutable();
+                await Cli.Wrap(vsCodeExecutable)
+                    .WithArguments(args => args
+                        .Add("--wait") // Crucial flag
+                        .Add(change.FilePath))
+                    .WithWorkingDirectory(projectRoot)
+                    .ExecuteAsync();
+                
+                var fullPath = Path.Combine(projectRoot, change.FilePath);
+                var fileContent = await File.ReadAllTextAsync(fullPath);
+                
+                if (fileContent.Contains("<<<<<<<") || fileContent.Contains("=======") || fileContent.Contains(">>>>>>>"))
+                {
+                    // The user closed the editor but did not resolve the conflict.
+                    resolve($"Conflict in '{change.FilePath}' was not resolved. Please try again.");
+                    return;
+                }
+                
+                var gitExecutable = FindGitExecutable();
+                
+                // The markers are gone, so now we can tell Git the conflict is resolved.
+                await Cli.Wrap(gitExecutable)
+                    .WithArguments($"add \"{change.FilePath}\"")
+                    .WithWorkingDirectory(projectRoot)
+                    .ExecuteAsync();
+
+                // The conflict is resolved and staged. Now, unstage the file to give the user control.
+                await Cli.Wrap(gitExecutable)
+                    .WithArguments($"reset HEAD \"{change.FilePath}\"")
+                    .WithWorkingDirectory(projectRoot)
+                    .ExecuteAsync();
+                    
+                resolve($"Successfully resolved conflict in '{change.FilePath}'. The file is now modified and ready for review.");
+            }
+            catch (Win32Exception ex)
+            {
+                reject(new Exception("Could not launch VS Code. Ensure it is installed and the 'code' command is available in your system's PATH.", ex));
+            }
+            catch (Exception ex)
+            {
+                reject(ex);
+            }
+        }
+        
+        /// <summary>
+        /// Executor for UnstageAllFilesIfSafe. Checks for conflicts and staged files, then unstages if appropriate.
+        /// </summary>
+        private static async void UnstageAllFilesIfSafeExecutor(Action<bool> resolve, Action<Exception> reject)
+        {
+            try
+            {
+                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
+                if (projectRoot == null)
+                {
+                    reject(new Exception("Could not find project root."));
+                    return;
+                }
+                using var repo = new Repository(projectRoot);
+
+                // Safety Check: Do not proceed if there are any conflicts.
+                if (repo.Index.Conflicts.Any())
+                {
+                    resolve(false);
+                    return;
+                }
+
+                // Check if there are any files staged for commit.
+                var stagedFiles = repo.RetrieveStatus().Where(s => s.State is 
+                    FileStatus.NewInIndex or 
+                    FileStatus.ModifiedInIndex or 
+                    FileStatus.DeletedFromIndex or 
+                    FileStatus.RenamedInIndex or 
+                    FileStatus.TypeChangeInIndex);
+                
+                if (!stagedFiles.Any())
+                {
+                    resolve(false); // Nothing to do.
+                    return;
+                }
+
+                // If we get here, it's safe to unstage everything.
+                await ExecuteGitCommandAsync("reset", projectRoot, null, 0, 141);
+                resolve(true); // Signal that files were unstaged.
+            }
+            catch (Exception ex)
+            {
+                reject(ex);
+            }
+        }
+        
+        /// <summary>
+        /// Searches for an executable in common macOS PATH directories.
+        /// </summary>
+        private static string FindExecutable(string name)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                // CliWrap handles PATH search on Windows automatically.
+                return name;
+            }
+            // For macOS/Linux, we need to be more explicit due to Unity's sandboxing.
+            string[] searchPaths = { "/usr/local/bin", "/usr/bin", "/bin", "/opt/homebrew/bin" };
+            foreach (var path in searchPaths)
+            {
+                var fullPath = Path.Combine(path, name);
+                if (File.Exists(fullPath))
+                {
+                    return fullPath;
+                }
+            }
+            throw new FileNotFoundException($"Could not find executable '{name}'. Please ensure it is installed and in your system's PATH.");
+        }
+        
+        private static string FindVsCodeExecutable() => FindExecutable("code");
+        private static string FindGitExecutable() => FindExecutable("git");
     }
 }

+ 17 - 1
Assets/Arbitrator/Editor/Settings/BetterGitSettings.cs

@@ -1,7 +1,23 @@
+using UnityEditor;
+
 namespace Terra.Arbitrator.Settings
 {
-    public class BetterGitSettings
+    public static class BetterGitSettings
     {
+        // Define unique keys to avoid conflicts in EditorPrefs.
+        private const string UsernameKey = "Terra.Arbitrator.Username";
+        private const string EmailKey = "Terra.Arbitrator.Email";
+
+        public static string Username
+        {
+            get => EditorPrefs.GetString(UsernameKey, "");
+            set => EditorPrefs.SetString(UsernameKey, value);
+        }
         
+        public static string Email
+        {
+            get => EditorPrefs.GetString(EmailKey, "");
+            set => EditorPrefs.SetString(EmailKey, value);
+        }
     }
 }

+ 3 - 0
Assets/Arbitrator/Editor/Settings/PullAnalysisRequest.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8529732aa9dc4125867d46b3e083b16c
+timeCreated: 1750677009

+ 2 - 1
Assets/Arbitrator/Editor/Terra.Arbitrator.asmdef

@@ -9,7 +9,8 @@
     "allowUnsafeCode": false,
     "overrideReferences": true,
     "precompiledReferences": [
-        "LibGit2Sharp.dll"
+        "LibGit2Sharp.dll",
+        "CliWrap.dll"
     ],
     "autoReferenced": false,
     "defineConstraints": [],

+ 58 - 55
Assets/GitMerge/Editor/ConflictWatcher.cs

@@ -3,76 +3,79 @@ using System.Collections.Generic;
 using System.IO;
 using UnityEditor;
 using UnityEngine;
-
+// Hello World
+// Hello World
+// Hello Here
+// Hello World
 namespace GitMerge
 {
     public class ConflictWatcher
-{
-    private static FileSystemWatcher watcher;
-    private static FileSystemEventArgs e;
-    
-    public ConflictWatcher()
     {
-        string assetPath = Application.dataPath;
-        watcher = new FileSystemWatcher(assetPath);
-        watcher.IncludeSubdirectories = true;
-        watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
-        watcher.Changed += OnFileChanged;
-        watcher.EnableRaisingEvents = true;
-    }
+        private static FileSystemWatcher watcher;
+        private static FileSystemEventArgs e;
 
-    public event Action<string> OnConflict;
-
-    private void OnFileChanged(object sender, FileSystemEventArgs _e)
-    {
-        e = _e;
-        EditorApplication.update += CheckWindow;
-    }
-
-    private void CheckWindow()
-    {
-        AssetDatabase.Refresh();
-        EditorApplication.update -= CheckWindow;
-        var path = e.FullPath;
-        var relativePath = "Assets" + path.Substring(Application.dataPath.Length);
-        
-        if (!path.EndsWith(".unity") && !path.EndsWith(".prefab")) return;
-        // Found it
-        UnityEngine.Object asset;
-        try
+        public ConflictWatcher()
         {
-            asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(relativePath);
+            string assetPath = Application.dataPath;
+            watcher = new FileSystemWatcher(assetPath);
+            watcher.IncludeSubdirectories = true;
+            watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
+            watcher.Changed += OnFileChanged;
+            watcher.EnableRaisingEvents = true;
         }
-        catch (Exception exception)
+
+        public event Action<string> OnConflict;
+
+        private void OnFileChanged(object sender, FileSystemEventArgs _e)
         {
-            Debug.Log(exception);
-            throw;
+            e = _e;
+            EditorApplication.update += CheckWindow;
         }
-        Debug.Log("asset" + asset);
-        if (asset != null)
+
+        private void CheckWindow()
         {
-            Debug.Log("Found asset" + asset.GetType());
-            bool isBroken = asset is BrokenPrefabAsset;
-                
-            // Open merge conflict window
-            if (isBroken)
+            AssetDatabase.Refresh();
+            EditorApplication.update -= CheckWindow;
+            var path = e.FullPath;
+            var relativePath = "Assets" + path.Substring(Application.dataPath.Length);
+
+            if (!path.EndsWith(".unity") && !path.EndsWith(".prefab")) return;
+            // Found it
+            UnityEngine.Object asset;
+            try
+            {
+                asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(relativePath);
+            }
+            catch (Exception exception)
             {
-                OnConflict?.Invoke(relativePath);
+                Debug.Log(exception);
+                throw;
+            }
+            Debug.Log("asset" + asset);
+            if (asset != null)
+            {
+                Debug.Log("Found asset" + asset.GetType());
+                bool isBroken = asset is BrokenPrefabAsset;
+
+                // Open merge conflict window
+                if (isBroken)
+                {
+                    OnConflict?.Invoke(relativePath);
+                }
             }
         }
-    }
-    
-    static bool IsOpenForEdit(string[] paths, List<string> outNotEditablePaths, StatusQueryOptions statusQueryOptions)
-    {
-        Debug.Log("IsOpenForEdit:");
-        // TODO: Do locking/unlocking here
 
-        foreach (var path in paths)
+        static bool IsOpenForEdit(string[] paths, List<string> outNotEditablePaths, StatusQueryOptions statusQueryOptions)
         {
-            // Check for lock
-            Debug.Log(path);
+            Debug.Log("IsOpenForEdit:");
+            // TODO: Do locking/unlocking here
+
+            foreach (var path in paths)
+            {
+                // Check for lock
+                Debug.Log(path);
+            }
+            return true;
         }
-        return true;
     }
-}    
 }

+ 7 - 5
Packages/packages-lock.json

@@ -25,8 +25,9 @@
     "com.unity.ext.nunit": {
       "version": "2.0.5",
       "depth": 1,
-      "source": "builtin",
-      "dependencies": {}
+      "source": "registry",
+      "dependencies": {},
+      "url": "https://packages.unity.com"
     },
     "com.unity.ide.rider": {
       "version": "3.0.36",
@@ -111,14 +112,15 @@
       }
     },
     "com.unity.test-framework": {
-      "version": "1.5.1",
+      "version": "1.4.6",
       "depth": 3,
-      "source": "builtin",
+      "source": "registry",
       "dependencies": {
         "com.unity.ext.nunit": "2.0.3",
         "com.unity.modules.imgui": "1.0.0",
         "com.unity.modules.jsonserialize": "1.0.0"
-      }
+      },
+      "url": "https://packages.unity.com"
     },
     "com.unity.test-framework.performance": {
       "version": "3.0.3",