// 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 _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(); window.titleContent = new GUIContent("Changes", EditorGUIUtility.IconContent("d_UnityEditor.VersionControl").image); } /// /// Called when the window is enabled. This triggers an automatic refresh /// when the window is opened or after scripts are recompiled. /// 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; } /// /// Draws the top menu bar for actions like refreshing. /// 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); } /// /// Draws the multi-column list of changed files. /// 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); } } } }