BranchAndStashManager.cs 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012
  1. using UnityEngine;
  2. using UnityEditor;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. namespace UnityVersionControl
  7. {
  8. /// <summary>
  9. /// Branch and Stash Management System
  10. /// Provides GitHub Desktop-style branch switching and stash management
  11. /// </summary>
  12. public class BranchAndStashManager : EditorWindow
  13. {
  14. #region Fields
  15. private Vector2 scrollPosition;
  16. private int selectedTab = 0;
  17. private readonly string[] tabNames = { "Branches", "Stashes" };
  18. // Branch data
  19. private readonly List<BranchInfo> branches = new List<BranchInfo>();
  20. private readonly List<StashInfo> stashes = new List<StashInfo>();
  21. private string currentBranch = "main";
  22. private string newBranchName = "";
  23. private bool showCreateBranchDialog = false;
  24. private bool showStashDialog = false;
  25. private string stashMessage = "";
  26. // UI state
  27. private string searchFilter = "";
  28. private bool showMergedBranches = false;
  29. private BranchInfo branchToDelete = null;
  30. private StashInfo stashToDelete = null;
  31. // Visual constants
  32. private static readonly Color backgroundColor = new Color(0.22f, 0.22f, 0.22f);
  33. private static readonly Color cardColor = new Color(0.28f, 0.28f, 0.28f);
  34. private static readonly Color accentColor = new Color(0.3f, 0.7f, 1f);
  35. private static readonly Color successColor = new Color(0.3f, 0.8f, 0.3f);
  36. private static readonly Color warningColor = new Color(1f, 0.8f, 0.2f);
  37. private static readonly Color errorColor = new Color(1f, 0.4f, 0.4f);
  38. private static readonly Color currentBranchColor = new Color(0.2f, 0.8f, 0.2f);
  39. private static readonly Color remoteBranchColor = new Color(0.6f, 0.6f, 1f);
  40. // Caches
  41. private readonly Dictionary<string, Texture2D> textureCache = new Dictionary<string, Texture2D>();
  42. #endregion
  43. #region Unity Lifecycle
  44. [MenuItem("Window/Version Control/Branch & Stash Manager", false, 2)]
  45. public static void ShowWindow()
  46. {
  47. var window = GetWindow<BranchAndStashManager>();
  48. window.titleContent = new GUIContent("Branches & Stashes");
  49. window.minSize = new Vector2(400, 500);
  50. window.Initialize();
  51. }
  52. private void OnEnable()
  53. {
  54. Initialize();
  55. }
  56. private void OnDisable()
  57. {
  58. CleanupTextures();
  59. }
  60. private void OnGUI()
  61. {
  62. DrawBackground();
  63. DrawHeader();
  64. DrawTabs();
  65. EditorGUILayout.Space(8);
  66. switch (selectedTab)
  67. {
  68. case 0: DrawBranchesTab(); break;
  69. case 1: DrawStashesTab(); break;
  70. }
  71. DrawDialogs();
  72. }
  73. #endregion
  74. #region Initialization
  75. private void Initialize()
  76. {
  77. LoadMockBranches();
  78. LoadMockStashes();
  79. }
  80. private void LoadMockBranches()
  81. {
  82. branches.Clear();
  83. // Current branch
  84. branches.Add(new BranchInfo
  85. {
  86. name = "main",
  87. isRemote = false,
  88. isCurrent = true,
  89. lastCommitMessage = "Initial commit",
  90. lastCommitAuthor = "Developer",
  91. lastCommitDate = DateTime.Now.AddDays(-1),
  92. commitHash = "a1b2c3d",
  93. aheadCount = 0,
  94. behindCount = 0
  95. });
  96. // Local branches
  97. branches.Add(new BranchInfo
  98. {
  99. name = "feature/player-movement",
  100. isRemote = false,
  101. isCurrent = false,
  102. lastCommitMessage = "Add player jump mechanics",
  103. lastCommitAuthor = "John Doe",
  104. lastCommitDate = DateTime.Now.AddHours(-3),
  105. commitHash = "b2c3d4e",
  106. aheadCount = 2,
  107. behindCount = 0
  108. });
  109. branches.Add(new BranchInfo
  110. {
  111. name = "bugfix/ui-scaling",
  112. isRemote = false,
  113. isCurrent = false,
  114. lastCommitMessage = "Fix UI scaling on different resolutions",
  115. lastCommitAuthor = "Jane Smith",
  116. lastCommitDate = DateTime.Now.AddHours(-5),
  117. commitHash = "c3d4e5f",
  118. aheadCount = 1,
  119. behindCount = 1
  120. });
  121. // Remote branches
  122. branches.Add(new BranchInfo
  123. {
  124. name = "origin/feature/audio-system",
  125. isRemote = true,
  126. isCurrent = false,
  127. lastCommitMessage = "Implement 3D spatial audio",
  128. lastCommitAuthor = "Bob Johnson",
  129. lastCommitDate = DateTime.Now.AddHours(-8),
  130. commitHash = "d4e5f6g",
  131. aheadCount = 0,
  132. behindCount = 3
  133. });
  134. branches.Add(new BranchInfo
  135. {
  136. name = "origin/feature/multiplayer",
  137. isRemote = true,
  138. isCurrent = false,
  139. lastCommitMessage = "Add network synchronization",
  140. lastCommitAuthor = "Alice Brown",
  141. lastCommitDate = DateTime.Now.AddDays(-2),
  142. commitHash = "e5f6g7h",
  143. aheadCount = 0,
  144. behindCount = 5
  145. });
  146. }
  147. private void LoadMockStashes()
  148. {
  149. stashes.Clear();
  150. stashes.Add(new StashInfo
  151. {
  152. id = "stash@{0}",
  153. message = "WIP: Player controller improvements",
  154. branchName = "feature/player-movement",
  155. author = "Developer",
  156. date = DateTime.Now.AddMinutes(-30),
  157. changedFiles = new List<string>
  158. {
  159. "Assets/Scripts/PlayerController.cs",
  160. "Assets/Scripts/InputManager.cs",
  161. "Assets/Prefabs/Player.prefab"
  162. }
  163. });
  164. stashes.Add(new StashInfo
  165. {
  166. id = "stash@{1}",
  167. message = "Experimental UI changes",
  168. branchName = "main",
  169. author = "Developer",
  170. date = DateTime.Now.AddHours(-2),
  171. changedFiles = new List<string>
  172. {
  173. "Assets/UI/MainMenu.prefab",
  174. "Assets/Materials/UI_Button.mat"
  175. }
  176. });
  177. stashes.Add(new StashInfo
  178. {
  179. id = "stash@{2}",
  180. message = "Audio volume adjustments",
  181. branchName = "feature/audio-system",
  182. author = "Developer",
  183. date = DateTime.Now.AddDays(-1),
  184. changedFiles = new List<string>
  185. {
  186. "Assets/Audio/AudioMixer.mixer",
  187. "Assets/Scripts/AudioManager.cs"
  188. }
  189. });
  190. }
  191. private void CleanupTextures()
  192. {
  193. foreach (var texture in textureCache.Values)
  194. {
  195. if (texture != null) DestroyImmediate(texture);
  196. }
  197. textureCache.Clear();
  198. }
  199. #endregion
  200. #region GUI Drawing
  201. private void DrawBackground()
  202. {
  203. var rect = new Rect(0, 0, position.width, position.height);
  204. EditorGUI.DrawRect(rect, backgroundColor);
  205. }
  206. private void DrawHeader()
  207. {
  208. DrawCard(() =>
  209. {
  210. using (new EditorGUILayout.HorizontalScope())
  211. {
  212. // Current branch indicator
  213. var currentBranchRect = GUILayoutUtility.GetRect(20, 20);
  214. var branchTexture = CreateRoundedTexture(currentBranchColor, 20, 10);
  215. GUI.DrawTexture(currentBranchRect, branchTexture);
  216. var branchIconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
  217. branchIconStyle.normal.textColor = Color.white;
  218. branchIconStyle.fontSize = 10;
  219. branchIconStyle.fontStyle = FontStyle.Bold;
  220. GUI.Label(currentBranchRect, "⎇", branchIconStyle);
  221. GUILayout.Space(8);
  222. using (new EditorGUILayout.VerticalScope())
  223. {
  224. var titleStyle = new GUIStyle(EditorStyles.boldLabel);
  225. titleStyle.fontSize = 14;
  226. titleStyle.normal.textColor = Color.white;
  227. EditorGUILayout.LabelField($"Current Branch: {currentBranch}", titleStyle);
  228. var currentBranchInfo = branches.FirstOrDefault(b => b.isCurrent);
  229. if (currentBranchInfo != null)
  230. {
  231. var subtitleStyle = new GUIStyle(EditorStyles.label);
  232. subtitleStyle.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
  233. subtitleStyle.fontSize = 11;
  234. var statusText = "";
  235. if (currentBranchInfo.aheadCount > 0)
  236. statusText += $"↑{currentBranchInfo.aheadCount} ";
  237. if (currentBranchInfo.behindCount > 0)
  238. statusText += $"↓{currentBranchInfo.behindCount} ";
  239. if (string.IsNullOrEmpty(statusText))
  240. statusText = "Up to date";
  241. EditorGUILayout.LabelField(statusText.Trim(), subtitleStyle);
  242. }
  243. }
  244. GUILayout.FlexibleSpace();
  245. // Quick actions
  246. var createBranchStyle = new GUIStyle(GUI.skin.button);
  247. createBranchStyle.normal.textColor = successColor;
  248. createBranchStyle.fixedHeight = 28;
  249. if (GUILayout.Button("+ NEW BRANCH", createBranchStyle, GUILayout.Width(100)))
  250. ShowCreateBranchDialog();
  251. GUILayout.Space(4);
  252. var stashStyle = new GUIStyle(GUI.skin.button);
  253. stashStyle.normal.textColor = warningColor;
  254. stashStyle.fixedHeight = 28;
  255. if (GUILayout.Button("📦 STASH", stashStyle, GUILayout.Width(80)))
  256. ShowStashDialog();
  257. }
  258. }, 12);
  259. }
  260. private void DrawTabs()
  261. {
  262. EditorGUILayout.Space(8);
  263. using (new EditorGUILayout.HorizontalScope())
  264. {
  265. for (int i = 0; i < tabNames.Length; i++)
  266. {
  267. var isSelected = selectedTab == i;
  268. var buttonStyle = new GUIStyle(GUI.skin.button);
  269. if (isSelected)
  270. {
  271. buttonStyle.normal.background = Texture2D.whiteTexture;
  272. buttonStyle.normal.textColor = backgroundColor;
  273. buttonStyle.fontStyle = FontStyle.Bold;
  274. }
  275. else
  276. {
  277. buttonStyle.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
  278. }
  279. buttonStyle.fixedHeight = 32;
  280. var tabLabel = tabNames[i];
  281. if (i == 0) tabLabel += $" ({branches.Count})";
  282. if (i == 1) tabLabel += $" ({stashes.Count})";
  283. if (GUILayout.Button(tabLabel, buttonStyle))
  284. selectedTab = i;
  285. }
  286. }
  287. }
  288. private void DrawBranchesTab()
  289. {
  290. EditorGUILayout.Space(8);
  291. DrawBranchFilters();
  292. DrawBranchList();
  293. }
  294. private void DrawBranchFilters()
  295. {
  296. DrawCard(() =>
  297. {
  298. using (new EditorGUILayout.HorizontalScope())
  299. {
  300. EditorGUILayout.LabelField("Search:", GUILayout.Width(50));
  301. searchFilter = EditorGUILayout.TextField(searchFilter);
  302. GUILayout.Space(8);
  303. var oldShowMerged = showMergedBranches;
  304. showMergedBranches = EditorGUILayout.Toggle("Show merged", showMergedBranches);
  305. if (oldShowMerged != showMergedBranches)
  306. Repaint();
  307. }
  308. }, 8);
  309. }
  310. private void DrawBranchList()
  311. {
  312. EditorGUILayout.Space(8);
  313. var filteredBranches = branches.Where(b =>
  314. string.IsNullOrEmpty(searchFilter) ||
  315. b.name.ToLower().Contains(searchFilter.ToLower())
  316. ).ToList();
  317. using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
  318. {
  319. scrollPosition = scrollView.scrollPosition;
  320. // Group branches by type
  321. var localBranches = filteredBranches.Where(b => !b.isRemote).ToList();
  322. var remoteBranches = filteredBranches.Where(b => b.isRemote).ToList();
  323. if (localBranches.Count > 0)
  324. {
  325. DrawBranchSection("Local Branches", localBranches);
  326. }
  327. if (remoteBranches.Count > 0)
  328. {
  329. EditorGUILayout.Space(12);
  330. DrawBranchSection("Remote Branches", remoteBranches);
  331. }
  332. }
  333. }
  334. private void DrawBranchSection(string title, List<BranchInfo> sectionBranches)
  335. {
  336. DrawCard(() =>
  337. {
  338. var sectionStyle = new GUIStyle(EditorStyles.boldLabel);
  339. sectionStyle.fontSize = 12;
  340. sectionStyle.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
  341. EditorGUILayout.LabelField(title, sectionStyle);
  342. EditorGUILayout.Space(8);
  343. foreach (var branch in sectionBranches)
  344. {
  345. DrawBranchItem(branch);
  346. if (branch != sectionBranches.Last())
  347. DrawSeparator(1f, 4f);
  348. }
  349. }, 12);
  350. }
  351. private void DrawBranchItem(BranchInfo branch)
  352. {
  353. using (new EditorGUILayout.HorizontalScope())
  354. {
  355. // Branch icon and name
  356. var iconSize = 24;
  357. var iconRect = GUILayoutUtility.GetRect(iconSize, iconSize);
  358. Color iconColor;
  359. string iconText;
  360. if (branch.isCurrent)
  361. {
  362. iconColor = currentBranchColor;
  363. iconText = "●";
  364. }
  365. else if (branch.isRemote)
  366. {
  367. iconColor = remoteBranchColor;
  368. iconText = "⊗";
  369. }
  370. else
  371. {
  372. iconColor = accentColor;
  373. iconText = "⎇";
  374. }
  375. var iconTexture = CreateRoundedTexture(iconColor, iconSize, iconSize / 2);
  376. GUI.DrawTexture(iconRect, iconTexture);
  377. var iconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
  378. iconStyle.normal.textColor = Color.white;
  379. iconStyle.fontSize = 10;
  380. iconStyle.fontStyle = FontStyle.Bold;
  381. GUI.Label(iconRect, iconText, iconStyle);
  382. GUILayout.Space(8);
  383. // Branch info
  384. using (new EditorGUILayout.VerticalScope())
  385. {
  386. using (new EditorGUILayout.HorizontalScope())
  387. {
  388. var nameStyle = new GUIStyle(EditorStyles.boldLabel);
  389. nameStyle.fontSize = 13;
  390. nameStyle.normal.textColor = branch.isCurrent ? currentBranchColor : Color.white;
  391. EditorGUILayout.LabelField(branch.name, nameStyle);
  392. GUILayout.FlexibleSpace();
  393. // Ahead/behind indicators
  394. if (branch.aheadCount > 0)
  395. {
  396. DrawCommitCountBadge($"↑{branch.aheadCount}", successColor);
  397. GUILayout.Space(4);
  398. }
  399. if (branch.behindCount > 0)
  400. {
  401. DrawCommitCountBadge($"↓{branch.behindCount}", warningColor);
  402. GUILayout.Space(4);
  403. }
  404. }
  405. var infoStyle = new GUIStyle(EditorStyles.label);
  406. infoStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
  407. infoStyle.fontSize = 10;
  408. var infoText = $"{branch.lastCommitMessage} • {branch.lastCommitAuthor} • {FormatDate(branch.lastCommitDate)}";
  409. EditorGUILayout.LabelField(infoText, infoStyle);
  410. }
  411. GUILayout.FlexibleSpace();
  412. // Actions
  413. DrawBranchActions(branch);
  414. }
  415. }
  416. private void DrawBranchActions(BranchInfo branch)
  417. {
  418. using (new EditorGUILayout.VerticalScope(GUILayout.Width(100)))
  419. {
  420. if (!branch.isCurrent)
  421. {
  422. var checkoutStyle = new GUIStyle(GUI.skin.button);
  423. checkoutStyle.normal.textColor = accentColor;
  424. checkoutStyle.fixedHeight = 22;
  425. if (GUILayout.Button("CHECKOUT", checkoutStyle))
  426. CheckoutBranch(branch);
  427. }
  428. if (!branch.isCurrent && !branch.isRemote)
  429. {
  430. var deleteStyle = new GUIStyle(GUI.skin.button);
  431. deleteStyle.normal.textColor = errorColor;
  432. deleteStyle.fixedHeight = 22;
  433. if (GUILayout.Button("DELETE", deleteStyle))
  434. branchToDelete = branch;
  435. }
  436. }
  437. }
  438. private void DrawStashesTab()
  439. {
  440. EditorGUILayout.Space(8);
  441. if (stashes.Count == 0)
  442. {
  443. DrawEmptyStashState();
  444. return;
  445. }
  446. using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
  447. {
  448. scrollPosition = scrollView.scrollPosition;
  449. DrawCard(() =>
  450. {
  451. foreach (var stash in stashes)
  452. {
  453. DrawStashItem(stash);
  454. if (stash != stashes.Last())
  455. DrawSeparator(1f, 8f);
  456. }
  457. }, 12);
  458. }
  459. }
  460. private void DrawStashItem(StashInfo stash)
  461. {
  462. using (new EditorGUILayout.HorizontalScope())
  463. {
  464. // Stash icon
  465. var iconSize = 32;
  466. var iconRect = GUILayoutUtility.GetRect(iconSize, iconSize);
  467. var iconTexture = CreateRoundedTexture(warningColor, iconSize, 6);
  468. GUI.DrawTexture(iconRect, iconTexture);
  469. var iconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
  470. iconStyle.normal.textColor = Color.white;
  471. iconStyle.fontSize = 14;
  472. iconStyle.fontStyle = FontStyle.Bold;
  473. GUI.Label(iconRect, "📦", iconStyle);
  474. GUILayout.Space(12);
  475. // Stash info
  476. using (new EditorGUILayout.VerticalScope())
  477. {
  478. var messageStyle = new GUIStyle(EditorStyles.boldLabel);
  479. messageStyle.fontSize = 13;
  480. messageStyle.normal.textColor = Color.white;
  481. EditorGUILayout.LabelField(stash.message, messageStyle);
  482. var detailStyle = new GUIStyle(EditorStyles.label);
  483. detailStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
  484. detailStyle.fontSize = 10;
  485. EditorGUILayout.LabelField($"Branch: {stash.branchName} • {stash.author} • {FormatDate(stash.date)}", detailStyle);
  486. // Changed files
  487. var filesText = $"{stash.changedFiles.Count} changed file{(stash.changedFiles.Count != 1 ? "s" : "")}";
  488. EditorGUILayout.LabelField(filesText, detailStyle);
  489. }
  490. GUILayout.FlexibleSpace();
  491. // Actions
  492. using (new EditorGUILayout.VerticalScope(GUILayout.Width(80)))
  493. {
  494. var applyStyle = new GUIStyle(GUI.skin.button);
  495. applyStyle.normal.textColor = successColor;
  496. applyStyle.fixedHeight = 22;
  497. if (GUILayout.Button("APPLY", applyStyle))
  498. ApplyStash(stash);
  499. var deleteStyle = new GUIStyle(GUI.skin.button);
  500. deleteStyle.normal.textColor = errorColor;
  501. deleteStyle.fixedHeight = 22;
  502. if (GUILayout.Button("DELETE", deleteStyle))
  503. stashToDelete = stash;
  504. }
  505. }
  506. }
  507. private void DrawEmptyStashState()
  508. {
  509. DrawCard(() =>
  510. {
  511. var iconRect = GUILayoutUtility.GetRect(48, 48);
  512. var emptyTexture = CreateRoundedTexture(new Color(0.5f, 0.5f, 0.5f, 0.3f), 48, 12);
  513. GUI.DrawTexture(iconRect, emptyTexture);
  514. var iconStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
  515. iconStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
  516. iconStyle.fontSize = 20;
  517. GUI.Label(iconRect, "📦", iconStyle);
  518. EditorGUILayout.Space(8);
  519. var titleStyle = new GUIStyle(EditorStyles.boldLabel);
  520. titleStyle.alignment = TextAnchor.MiddleCenter;
  521. titleStyle.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
  522. EditorGUILayout.LabelField("No Stashes", titleStyle);
  523. var subtitleStyle = new GUIStyle(EditorStyles.label);
  524. subtitleStyle.alignment = TextAnchor.MiddleCenter;
  525. subtitleStyle.normal.textColor = new Color(0.6f, 0.6f, 0.6f);
  526. subtitleStyle.wordWrap = true;
  527. EditorGUILayout.LabelField("Stash your work-in-progress changes to switch branches safely.", subtitleStyle);
  528. }, 32);
  529. }
  530. private void DrawDialogs()
  531. {
  532. if (showCreateBranchDialog)
  533. DrawCreateBranchDialog();
  534. if (showStashDialog)
  535. DrawStashDialog();
  536. if (branchToDelete != null)
  537. DrawDeleteBranchDialog();
  538. if (stashToDelete != null)
  539. DrawDeleteStashDialog();
  540. }
  541. private void DrawCreateBranchDialog()
  542. {
  543. var dialogRect = new Rect(
  544. (position.width - 300) * 0.5f,
  545. (position.height - 150) * 0.5f,
  546. 300, 150
  547. );
  548. EditorGUI.DrawRect(dialogRect, new Color(0, 0, 0, 0.7f));
  549. GUILayout.BeginArea(dialogRect);
  550. DrawCard(() =>
  551. {
  552. EditorGUILayout.LabelField("Create New Branch", EditorStyles.boldLabel);
  553. EditorGUILayout.Space(8);
  554. EditorGUILayout.LabelField("Branch name:");
  555. newBranchName = EditorGUILayout.TextField(newBranchName);
  556. EditorGUILayout.Space(12);
  557. using (new EditorGUILayout.HorizontalScope())
  558. {
  559. if (GUILayout.Button("Create"))
  560. {
  561. CreateBranch(newBranchName);
  562. showCreateBranchDialog = false;
  563. newBranchName = "";
  564. }
  565. if (GUILayout.Button("Cancel"))
  566. {
  567. showCreateBranchDialog = false;
  568. newBranchName = "";
  569. }
  570. }
  571. }, 12);
  572. GUILayout.EndArea();
  573. }
  574. private void DrawStashDialog()
  575. {
  576. var dialogRect = new Rect(
  577. (position.width - 350) * 0.5f,
  578. (position.height - 180) * 0.5f,
  579. 350, 180
  580. );
  581. EditorGUI.DrawRect(dialogRect, new Color(0, 0, 0, 0.7f));
  582. GUILayout.BeginArea(dialogRect);
  583. DrawCard(() =>
  584. {
  585. EditorGUILayout.LabelField("Create Stash", EditorStyles.boldLabel);
  586. EditorGUILayout.Space(8);
  587. EditorGUILayout.LabelField("Stash message:");
  588. stashMessage = EditorGUILayout.TextArea(stashMessage, GUILayout.Height(50));
  589. EditorGUILayout.Space(12);
  590. using (new EditorGUILayout.HorizontalScope())
  591. {
  592. if (GUILayout.Button("Create Stash"))
  593. {
  594. CreateStash(stashMessage);
  595. showStashDialog = false;
  596. stashMessage = "";
  597. }
  598. if (GUILayout.Button("Cancel"))
  599. {
  600. showStashDialog = false;
  601. stashMessage = "";
  602. }
  603. }
  604. }, 12);
  605. GUILayout.EndArea();
  606. }
  607. private void DrawDeleteBranchDialog()
  608. {
  609. if (EditorUtility.DisplayDialog(
  610. "Delete Branch",
  611. $"Are you sure you want to delete branch '{branchToDelete.name}'?\n\nThis action cannot be undone.",
  612. "Delete",
  613. "Cancel"))
  614. {
  615. DeleteBranch(branchToDelete);
  616. }
  617. branchToDelete = null;
  618. }
  619. private void DrawDeleteStashDialog()
  620. {
  621. if (EditorUtility.DisplayDialog(
  622. "Delete Stash",
  623. $"Are you sure you want to delete stash '{stashToDelete.message}'?\n\nThis action cannot be undone.",
  624. "Delete",
  625. "Cancel"))
  626. {
  627. DeleteStash(stashToDelete);
  628. }
  629. stashToDelete = null;
  630. }
  631. #endregion
  632. #region Helper Methods
  633. private void DrawCard(System.Action content, int padding = 12)
  634. {
  635. var rect = EditorGUILayout.BeginVertical();
  636. EditorGUI.DrawRect(rect, cardColor);
  637. GUILayout.Space(padding);
  638. EditorGUILayout.BeginHorizontal();
  639. GUILayout.Space(padding);
  640. EditorGUILayout.BeginVertical();
  641. content?.Invoke();
  642. EditorGUILayout.EndVertical();
  643. GUILayout.Space(padding);
  644. EditorGUILayout.EndHorizontal();
  645. GUILayout.Space(padding);
  646. EditorGUILayout.EndVertical();
  647. }
  648. private void DrawSeparator(float thickness = 1f, float spacing = 8f)
  649. {
  650. EditorGUILayout.Space(spacing);
  651. var rect = EditorGUILayout.GetControlRect(false, thickness);
  652. EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.3f));
  653. EditorGUILayout.Space(spacing);
  654. }
  655. private void DrawCommitCountBadge(string text, Color color)
  656. {
  657. var badgeStyle = new GUIStyle(GUI.skin.box);
  658. badgeStyle.normal.textColor = Color.white;
  659. badgeStyle.normal.background = CreateRoundedTexture(color, 16, 8);
  660. badgeStyle.fontSize = 9;
  661. badgeStyle.padding = new RectOffset(6, 6, 2, 2);
  662. badgeStyle.margin = new RectOffset(0, 0, 0, 0);
  663. GUILayout.Label(text, badgeStyle);
  664. }
  665. private Texture2D CreateRoundedTexture(Color color, int size = 64, int cornerRadius = 8)
  666. {
  667. var key = $"{color}_{size}_{cornerRadius}";
  668. if (textureCache.TryGetValue(key, out var cachedTexture) && cachedTexture != null)
  669. return cachedTexture;
  670. var texture = new Texture2D(size, size);
  671. var pixels = new Color[size * size];
  672. for (int y = 0; y < size; y++)
  673. {
  674. for (int x = 0; x < size; x++)
  675. {
  676. float distanceToCorner = float.MaxValue;
  677. if (x < cornerRadius && y < cornerRadius)
  678. distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(cornerRadius, cornerRadius));
  679. else if (x >= size - cornerRadius && y < cornerRadius)
  680. distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(size - cornerRadius - 1, cornerRadius));
  681. else if (x < cornerRadius && y >= size - cornerRadius)
  682. distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(cornerRadius, size - cornerRadius - 1));
  683. else if (x >= size - cornerRadius && y >= size - cornerRadius)
  684. distanceToCorner = Vector2.Distance(new Vector2(x, y), new Vector2(size - cornerRadius - 1, size - cornerRadius - 1));
  685. pixels[y * size + x] = (distanceToCorner <= cornerRadius ||
  686. (x >= cornerRadius && x < size - cornerRadius) ||
  687. (y >= cornerRadius && y < size - cornerRadius)) ? color : Color.clear;
  688. }
  689. }
  690. texture.SetPixels(pixels);
  691. texture.Apply();
  692. textureCache[key] = texture;
  693. return texture;
  694. }
  695. private string FormatDate(DateTime date)
  696. {
  697. var diff = DateTime.Now - date;
  698. if (diff.TotalMinutes < 1) return "just now";
  699. if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m ago";
  700. if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h ago";
  701. if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d ago";
  702. return date.ToString("MMM dd");
  703. }
  704. private void ShowCreateBranchDialog()
  705. {
  706. showCreateBranchDialog = true;
  707. }
  708. private void ShowStashDialog()
  709. {
  710. showStashDialog = true;
  711. }
  712. #endregion
  713. #region Branch Operations
  714. private void CheckoutBranch(BranchInfo branch)
  715. {
  716. try
  717. {
  718. Debug.Log($"🔄 Checking out branch: {branch.name}");
  719. // Update current branch
  720. foreach (var b in branches)
  721. b.isCurrent = false;
  722. branch.isCurrent = true;
  723. currentBranch = branch.name;
  724. EditorUtility.DisplayDialog("Branch Checkout",
  725. $"Successfully checked out branch '{branch.name}'",
  726. "OK");
  727. Repaint();
  728. }
  729. catch (Exception ex)
  730. {
  731. EditorUtility.DisplayDialog("Checkout Failed",
  732. $"Failed to checkout branch: {ex.Message}",
  733. "OK");
  734. }
  735. }
  736. private void CreateBranch(string branchName)
  737. {
  738. if (string.IsNullOrWhiteSpace(branchName))
  739. {
  740. EditorUtility.DisplayDialog("Invalid Branch Name",
  741. "Branch name cannot be empty.",
  742. "OK");
  743. return;
  744. }
  745. if (branches.Any(b => b.name == branchName))
  746. {
  747. EditorUtility.DisplayDialog("Branch Exists",
  748. $"A branch named '{branchName}' already exists.",
  749. "OK");
  750. return;
  751. }
  752. var newBranch = new BranchInfo
  753. {
  754. name = branchName,
  755. isRemote = false,
  756. isCurrent = false,
  757. lastCommitMessage = "Branch created",
  758. lastCommitAuthor = "Developer",
  759. lastCommitDate = DateTime.Now,
  760. commitHash = Guid.NewGuid().ToString().Substring(0, 7),
  761. aheadCount = 0,
  762. behindCount = 0
  763. };
  764. branches.Insert(1, newBranch); // Insert after main branch
  765. Debug.Log($"✅ Created new branch: {branchName}");
  766. EditorUtility.DisplayDialog("Branch Created",
  767. $"Successfully created branch '{branchName}'",
  768. "OK");
  769. Repaint();
  770. }
  771. private void DeleteBranch(BranchInfo branch)
  772. {
  773. branches.Remove(branch);
  774. Debug.Log($"🗑️ Deleted branch: {branch.name}");
  775. EditorUtility.DisplayDialog("Branch Deleted",
  776. $"Successfully deleted branch '{branch.name}'",
  777. "OK");
  778. Repaint();
  779. }
  780. #endregion
  781. #region Stash Operations
  782. private void CreateStash(string message)
  783. {
  784. if (string.IsNullOrWhiteSpace(message))
  785. message = "WIP: Stashed changes";
  786. var newStash = new StashInfo
  787. {
  788. id = $"stash@{{{stashes.Count}}}",
  789. message = message,
  790. branchName = currentBranch,
  791. author = "Developer",
  792. date = DateTime.Now,
  793. changedFiles = new List<string> { "Assets/Scripts/TempFile.cs" } // Mock changed files
  794. };
  795. stashes.Insert(0, newStash);
  796. Debug.Log($"📦 Created stash: {message}");
  797. EditorUtility.DisplayDialog("Stash Created",
  798. $"Successfully stashed changes: '{message}'",
  799. "OK");
  800. Repaint();
  801. }
  802. private void ApplyStash(StashInfo stash)
  803. {
  804. Debug.Log($"📥 Applying stash: {stash.message}");
  805. EditorUtility.DisplayDialog("Stash Applied",
  806. $"Successfully applied stash: '{stash.message}'",
  807. "OK");
  808. // In real implementation, this would apply the stashed changes
  809. // For now, we'll just show the dialog
  810. Repaint();
  811. }
  812. private void DeleteStash(StashInfo stash)
  813. {
  814. stashes.Remove(stash);
  815. Debug.Log($"🗑️ Deleted stash: {stash.message}");
  816. EditorUtility.DisplayDialog("Stash Deleted",
  817. $"Successfully deleted stash: '{stash.message}'",
  818. "OK");
  819. Repaint();
  820. }
  821. #endregion
  822. }
  823. #region Data Models
  824. [Serializable]
  825. public class BranchInfo
  826. {
  827. public string name;
  828. public bool isRemote;
  829. public bool isCurrent;
  830. public string lastCommitMessage;
  831. public string lastCommitAuthor;
  832. public DateTime lastCommitDate;
  833. public string commitHash;
  834. public int aheadCount;
  835. public int behindCount;
  836. }
  837. [Serializable]
  838. public class StashInfo
  839. {
  840. public string id;
  841. public string message;
  842. public string branchName;
  843. public string author;
  844. public DateTime date;
  845. public List<string> changedFiles;
  846. }
  847. #endregion
  848. }