// 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 Terra.Arbitrator.Services; using System.Collections.Generic; namespace Terra.Arbitrator.GUI { 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 = ""; private GUIStyle _evenRowStyle; private bool _stylesInitialized; [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(); } /// /// Initializes custom GUIStyles. We do this here to avoid creating new /// styles and textures on every OnGUI call, which is inefficient. /// private void InitializeStyles() { if (_stylesInitialized) return; _evenRowStyle = new GUIStyle(); // Create a 1x1 texture with a subtle gray color var texture = new Texture2D(1, 1); // Use a slightly different color depending on the editor skin (light/dark) var color = EditorGUIUtility.isProSkin ? new Color(0.3f, 0.3f, 0.3f, 0.3f) : new Color(0.8f, 0.8f, 0.8f, 0.5f); texture.SetPixel(0, 0, color); texture.Apply(); _evenRowStyle.normal.background = texture; _stylesInitialized = true; } private void OnGUI() { InitializeStyles(); // --- 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 --- else if (!_isLoading && _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); EditorGUI.BeginDisabledGroup(_isLoading); // 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(); } if (_changes != null && _changes.Count > 0) { if (GUILayout.Button("Select All", EditorStyles.toolbarButton, GUILayout.Width(80))) { SetAllSelection(true); } if (GUILayout.Button("Deselect All", EditorStyles.toolbarButton, GUILayout.Width(80))) { SetAllSelection(false); } } EditorGUI.EndDisabledGroup(); // 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 SetAllSelection(bool selected) { if (_changes == null) return; foreach (var change in _changes) { change.IsSelectedForCommit = selected; } } 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)); for (var i = 0; i < _changes.Count; i++) { var change = _changes[i]; // Use the evenRowStyle for every second row (i % 2 == 0), otherwise use no style. var rowStyle = i % 2 == 0 ? _evenRowStyle : GUIStyle.none; EditorGUILayout.BeginHorizontal(rowStyle); change.IsSelectedForCommit = EditorGUILayout.Toggle(change.IsSelectedForCommit, GUILayout.Width(45)); string status; Color statusColor; string filePathDisplay; switch (change.Status) { case ChangeKind.Added: status = "[+]"; statusColor = Color.green; filePathDisplay = change.FilePath; break; case ChangeKind.Deleted: status = "[-]"; statusColor = Color.red; filePathDisplay = change.FilePath; break; case ChangeKind.Modified: status = "[M]"; statusColor = new Color(1.0f, 0.6f, 0.0f); filePathDisplay = change.FilePath; break; case 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: default: status = "[?]"; statusColor = Color.white; filePathDisplay = change.FilePath; break; } var originalColor = UnityEngine.GUI.color; UnityEngine.GUI.color = statusColor; EditorGUILayout.LabelField(new GUIContent(status, change.Status.ToString()), GUILayout.Width(50)); UnityEngine.GUI.color = originalColor; EditorGUILayout.LabelField(new GUIContent(filePathDisplay, filePathDisplay)); EditorGUI.BeginDisabledGroup(_isLoading); if (GUILayout.Button(new GUIContent("Reset", "Revert changes for this file"), GUILayout.Width(55))) { 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))) { Debug.Log("Commit & Push Selected Files"); HandleCommitAndPush(); } EditorGUI.EndDisabledGroup(); if (isPushDisabled) { EditorGUILayout.HelpBox("Please enter a commit message and select at least one file.", MessageType.Warning); } } } }