ArbitratorWindow.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. // Copyright (c) 2025 TerraByte Inc.
  2. //
  3. // This script creates a custom Unity Editor window called "Arbitrator" to compare
  4. // local Git changes with the tracked remote branch using the LibGit2Sharp library.
  5. //
  6. // HOW TO USE:
  7. // 1. Ensure you have manually installed the LibGit2Sharp v0.27.0 package.
  8. // 2. Create an "Editor" folder in your Assets directory if you don't have one.
  9. // 3. Save this script as "Arbitrator.cs" inside the "Editor" folder.
  10. // 4. In Unity, open the window from the top menu: Terra > Arbitrator.
  11. // 5. Click the "Compare with Cloud" button. Results will appear in the console.
  12. using System.Linq;
  13. using UnityEngine;
  14. using UnityEditor;
  15. using LibGit2Sharp;
  16. using Terra.Arbitrator.Services;
  17. using System.Collections.Generic;
  18. namespace Terra.Arbitrator.GUI
  19. {
  20. public class ArbitratorWindow : EditorWindow
  21. {
  22. private List<GitChange> _changes;
  23. private string _commitMessage = "";
  24. private Vector2 _scrollPosition;
  25. private string _infoMessage;
  26. private string _errorMessage;
  27. private bool _isLoading;
  28. private string _loadingMessage = "";
  29. private GUIStyle _evenRowStyle;
  30. private bool _stylesInitialized;
  31. [MenuItem("Terra/Changes")]
  32. public static void ShowWindow()
  33. {
  34. var window = GetWindow<ArbitratorWindow>();
  35. window.titleContent = new GUIContent("Changes", EditorGUIUtility.IconContent("d_UnityEditor.VersionControl").image);
  36. }
  37. /// <summary>
  38. /// Called when the window is enabled. This triggers an automatic refresh
  39. /// when the window is opened or after scripts are recompiled.
  40. /// </summary>
  41. private void OnEnable()
  42. {
  43. HandleCompare();
  44. }
  45. /// <summary>
  46. /// Initializes custom GUIStyles. We do this here to avoid creating new
  47. /// styles and textures on every OnGUI call, which is inefficient.
  48. /// </summary>
  49. private void InitializeStyles()
  50. {
  51. if (_stylesInitialized) return;
  52. _evenRowStyle = new GUIStyle();
  53. // Create a 1x1 texture with a subtle gray color
  54. var texture = new Texture2D(1, 1);
  55. // Use a slightly different color depending on the editor skin (light/dark)
  56. var color = EditorGUIUtility.isProSkin
  57. ? new Color(0.3f, 0.3f, 0.3f, 0.3f)
  58. : new Color(0.8f, 0.8f, 0.8f, 0.5f);
  59. texture.SetPixel(0, 0, color);
  60. texture.Apply();
  61. _evenRowStyle.normal.background = texture;
  62. _stylesInitialized = true;
  63. }
  64. private void OnGUI()
  65. {
  66. InitializeStyles();
  67. // --- Top Toolbar ---
  68. DrawToolbar();
  69. EditorGUILayout.Space();
  70. // --- Message Display Area ---
  71. if (!string.IsNullOrEmpty(_errorMessage))
  72. {
  73. EditorGUILayout.HelpBox(_errorMessage, MessageType.Error);
  74. }
  75. else if (!string.IsNullOrEmpty(_infoMessage) && !_isLoading)
  76. {
  77. // Only show an info message if not loading, to prevent flicker
  78. EditorGUILayout.HelpBox(_infoMessage, MessageType.Info);
  79. }
  80. // --- Main Content ---
  81. else if (!_isLoading && _changes is { Count: > 0 })
  82. {
  83. DrawChangesList();
  84. DrawCommitSection();
  85. }
  86. }
  87. private void ClearMessages()
  88. {
  89. _errorMessage = null;
  90. _infoMessage = null;
  91. }
  92. /// <summary>
  93. /// Draws the top menu bar for actions like refreshing.
  94. /// </summary>
  95. private void DrawToolbar()
  96. {
  97. EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
  98. EditorGUI.BeginDisabledGroup(_isLoading);
  99. // The refresh button is now on the toolbar
  100. if (GUILayout.Button(new GUIContent("Refresh", EditorGUIUtility.IconContent("Refresh").image, "Fetches the latest status from the remote."), EditorStyles.toolbarButton, GUILayout.Width(80)))
  101. {
  102. HandleCompare();
  103. }
  104. if (_changes != null && _changes.Count > 0)
  105. {
  106. if (GUILayout.Button("Select All", EditorStyles.toolbarButton, GUILayout.Width(80)))
  107. {
  108. SetAllSelection(true);
  109. }
  110. if (GUILayout.Button("Deselect All", EditorStyles.toolbarButton, GUILayout.Width(80)))
  111. {
  112. SetAllSelection(false);
  113. }
  114. }
  115. EditorGUI.EndDisabledGroup();
  116. // This pushes everything that comes after it to the right.
  117. GUILayout.FlexibleSpace();
  118. // The loading message will appear on the right side of the toolbar.
  119. if (_isLoading)
  120. {
  121. GUILayout.Label(_loadingMessage);
  122. }
  123. // Future: Add a dropdown menu for filters or settings here.
  124. // If (GUILayout.Button("Filters", EditorStyles.toolbarDropDown)) { ... }
  125. EditorGUILayout.EndHorizontal();
  126. }
  127. private void SetAllSelection(bool selected)
  128. {
  129. if (_changes == null) return;
  130. foreach (var change in _changes)
  131. {
  132. change.IsSelectedForCommit = selected;
  133. }
  134. }
  135. private void HandleCompare()
  136. {
  137. _isLoading = true;
  138. _loadingMessage = "Comparing with remote repository...";
  139. ClearMessages();
  140. _changes = null;
  141. GitService.CompareLocalToRemote()
  142. .Then(result => {
  143. _changes = result;
  144. if (_changes.Count == 0)
  145. {
  146. _infoMessage = "You are up-to-date! No local changes detected.";
  147. }
  148. })
  149. .Catch(ex => {
  150. _errorMessage = $"Comparison Failed: {ex.Message}";
  151. })
  152. .Finally(() => {
  153. _isLoading = false;
  154. Repaint(); // Redraw the window with the new state
  155. });
  156. }
  157. private void HandleCommitAndPush()
  158. {
  159. _isLoading = true;
  160. _loadingMessage = "Staging, committing, and pushing files...";
  161. ClearMessages();
  162. var selectedFiles = _changes.Where(c => c.IsSelectedForCommit).ToList();
  163. GitService.CommitAndPush(selectedFiles, _commitMessage)
  164. .Then(successMessage => {
  165. _infoMessage = successMessage;
  166. _commitMessage = ""; // Clear message on success
  167. _changes = null; // Clear the list, forcing a refresh
  168. HandleCompare(); // Automatically refresh to confirm
  169. })
  170. .Catch(ex => {
  171. _errorMessage = $"Push Failed: {ex.Message}";
  172. })
  173. .Finally(() => {
  174. _isLoading = false;
  175. // Repaint is handled by the chained HandleCompare call on success
  176. if (!string.IsNullOrEmpty(_errorMessage)) Repaint();
  177. });
  178. }
  179. private void HandleResetFile(GitChange change)
  180. {
  181. if (_isLoading) return;
  182. _isLoading = true;
  183. _loadingMessage = $"Generating diff for {change.FilePath}...";
  184. ClearMessages();
  185. Repaint();
  186. // Step 1: Get the diff content for the file.
  187. GitService.GetFileDiff(change)
  188. .Then(diffContent =>
  189. {
  190. // This callback runs on the main thread when the diff is ready.
  191. // Step 2: Show the modal diff window.
  192. DiffWindow.ShowWindow(change.FilePath, diffContent, wasConfirmed =>
  193. {
  194. // This callback runs after the diff window is closed.
  195. if (!wasConfirmed)
  196. {
  197. _isLoading = false; // User cancelled.
  198. Repaint();
  199. return;
  200. }
  201. // Step 3: User confirmed. Proceed to reset the file.
  202. _loadingMessage = $"Resetting {change.FilePath}...";
  203. Repaint();
  204. GitService.ResetFileChanges(change)
  205. .Then(successMessage => {
  206. _infoMessage = successMessage;
  207. HandleCompare(); // Refresh the main list.
  208. })
  209. .Catch(ex => {
  210. _errorMessage = $"Reset Failed: {ex.Message}";
  211. _isLoading = false;
  212. Repaint();
  213. });
  214. });
  215. })
  216. .Catch(ex =>
  217. {
  218. // This runs if getting the diff itself failed.
  219. _errorMessage = $"Could not generate diff: {ex.Message}";
  220. _isLoading = false;
  221. Repaint();
  222. })
  223. .Finally(HandleCompare);
  224. }
  225. /// <summary>
  226. /// Draws the multi-column list of changed files.
  227. /// </summary>
  228. private void DrawChangesList()
  229. {
  230. // --- Draw Header ---
  231. EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
  232. EditorGUILayout.LabelField("Commit", GUILayout.Width(45));
  233. EditorGUILayout.LabelField("Status", GUILayout.Width(50));
  234. EditorGUILayout.LabelField("File Path");
  235. EditorGUILayout.LabelField("Actions", GUILayout.Width(55));
  236. EditorGUILayout.EndHorizontal();
  237. // --- Draw Scrollable List ---
  238. _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.ExpandHeight(true));
  239. for (var i = 0; i < _changes.Count; i++)
  240. {
  241. var change = _changes[i];
  242. // Use the evenRowStyle for every second row (i % 2 == 0), otherwise use no style.
  243. var rowStyle = i % 2 == 0 ? _evenRowStyle : GUIStyle.none;
  244. EditorGUILayout.BeginHorizontal(rowStyle);
  245. change.IsSelectedForCommit = EditorGUILayout.Toggle(change.IsSelectedForCommit, GUILayout.Width(45));
  246. string status;
  247. Color statusColor;
  248. string filePathDisplay;
  249. switch (change.Status)
  250. {
  251. case ChangeKind.Added:
  252. status = "[+]"; statusColor = Color.green; filePathDisplay = change.FilePath; break;
  253. case ChangeKind.Deleted:
  254. status = "[-]"; statusColor = Color.red; filePathDisplay = change.FilePath; break;
  255. case ChangeKind.Modified:
  256. status = "[M]"; statusColor = new Color(1.0f, 0.6f, 0.0f); filePathDisplay = change.FilePath; break;
  257. case ChangeKind.Renamed:
  258. status = "[R]"; statusColor = new Color(0.6f, 0.6f, 1.0f);
  259. filePathDisplay = $"{change.OldFilePath} -> {change.FilePath}"; break;
  260. case ChangeKind.Unmodified:
  261. case ChangeKind.Copied:
  262. case ChangeKind.Ignored:
  263. case ChangeKind.Untracked:
  264. case ChangeKind.TypeChanged:
  265. case ChangeKind.Unreadable:
  266. case ChangeKind.Conflicted:
  267. default:
  268. status = "[?]"; statusColor = Color.white; filePathDisplay = change.FilePath; break;
  269. }
  270. var originalColor = UnityEngine.GUI.color;
  271. UnityEngine.GUI.color = statusColor;
  272. EditorGUILayout.LabelField(new GUIContent(status, change.Status.ToString()), GUILayout.Width(50));
  273. UnityEngine.GUI.color = originalColor;
  274. EditorGUILayout.LabelField(new GUIContent(filePathDisplay, filePathDisplay));
  275. EditorGUI.BeginDisabledGroup(_isLoading);
  276. if (GUILayout.Button(new GUIContent("Reset", "Revert changes for this file"), GUILayout.Width(55)))
  277. {
  278. EditorApplication.delayCall += () => HandleResetFile(change);
  279. }
  280. EditorGUI.EndDisabledGroup();
  281. EditorGUILayout.EndHorizontal();
  282. }
  283. EditorGUILayout.EndScrollView();
  284. }
  285. private void DrawCommitSection()
  286. {
  287. EditorGUILayout.Space(10);
  288. EditorGUILayout.LabelField("Commit & Push", EditorStyles.boldLabel);
  289. _commitMessage = EditorGUILayout.TextArea(_commitMessage, GUILayout.Height(60), GUILayout.ExpandWidth(true));
  290. var isPushDisabled = string.IsNullOrWhiteSpace(_commitMessage) || !_changes.Any(c => c.IsSelectedForCommit);
  291. EditorGUI.BeginDisabledGroup(isPushDisabled);
  292. if (GUILayout.Button("Commit & Push Selected Files", GUILayout.Height(40)))
  293. {
  294. Debug.Log("Commit & Push Selected Files");
  295. HandleCommitAndPush();
  296. }
  297. EditorGUI.EndDisabledGroup();
  298. if (isPushDisabled)
  299. {
  300. EditorGUILayout.HelpBox("Please enter a commit message and select at least one file.", MessageType.Warning);
  301. }
  302. }
  303. }
  304. }