#if UNITY_EDITOR using System; using System.IO; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; [InitializeOnLoad] public static class GitBranchToolbar { private const string ToolbarElementName = "GitBranchToolbar"; private const double RefreshInterval = 2.0; private const double ToolbarCheckInterval = 0.25; private const float Height = 18f; private static VisualElement toolbarElement; private static string cachedBranchName = "..."; private static GitState cachedState = GitState.Unknown; private static double nextRefreshTime; private static double nextToolbarCheckTime; private enum GitState { Unknown, NoRepository, Branch, DetachedHead } static GitBranchToolbar() { RefreshBranchInfo(force: true); EditorApplication.update += OnEditorUpdate; EditorApplication.delayCall += AddToolbarUI; AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; } private static void OnAfterAssemblyReload() { EditorApplication.delayCall += AddToolbarUI; } private static void OnEditorUpdate() { if (EditorApplication.timeSinceStartup >= nextRefreshTime) { RefreshBranchInfo(); } EnsureToolbarExists(); } private static void EnsureToolbarExists() { if (EditorApplication.timeSinceStartup < nextToolbarCheckTime) return; nextToolbarCheckTime = EditorApplication.timeSinceStartup + ToolbarCheckInterval; var toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar"); if (toolbarType == null) return; var toolbars = Resources.FindObjectsOfTypeAll(toolbarType); if (toolbars == null || toolbars.Length == 0) return; foreach (var toolbar in toolbars) { TryInjectIntoToolbar(toolbarType, toolbar); } } private static void AddToolbarUI() { var toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar"); if (toolbarType == null) return; var toolbars = Resources.FindObjectsOfTypeAll(toolbarType); if (toolbars == null || toolbars.Length == 0) return; foreach (var toolbar in toolbars) { TryInjectIntoToolbar(toolbarType, toolbar); } } private static void TryInjectIntoToolbar(Type toolbarType, UnityEngine.Object toolbar) { var rootField = toolbarType.GetField("m_Root", BindingFlags.NonPublic | BindingFlags.Instance); if (rootField == null) return; if (rootField.GetValue(toolbar) is not VisualElement root) return; var rightContainer = root.Q("ToolbarZoneRightAlign"); if (rightContainer == null) return; var existing = rightContainer.Q(ToolbarElementName); if (existing != null) { toolbarElement = existing; return; } var newElement = new IMGUIContainer(DrawGUI); newElement.name = ToolbarElementName; newElement.style.flexShrink = 0; newElement.style.flexGrow = 0; newElement.style.minWidth = 70; newElement.style.maxWidth = 130; newElement.style.marginLeft = 4; newElement.style.marginRight = 0; newElement.style.overflow = Overflow.Hidden; newElement.style.alignSelf = Align.FlexEnd; rightContainer.Add(newElement); toolbarElement = newElement; } private static void DrawGUI() { RefreshBranchInfo(); var indicatorColor = GetBackgroundColor(cachedState, cachedBranchName); var textColor = GetTextColor(cachedState); string displayText = GetShortBranchName(cachedBranchName, cachedState); Rect rect = GUILayoutUtility.GetRect( GUIContent.none, GUIStyle.none, GUILayout.Height(Height), GUILayout.MinWidth(80), GUILayout.MaxWidth(220) ); const float dotSize = 8f; const float leftPadding = 4f; const float gap = 6f; const float rightPadding = 0f; Rect dotRect = new Rect( rect.x + leftPadding, rect.y + Mathf.Round((rect.height - dotSize) * 0.5f), dotSize, dotSize ); Rect labelRect = new Rect( dotRect.xMax + gap, rect.y, rect.width - leftPadding - dotSize - gap - rightPadding, rect.height ); GUI.Label( labelRect, new GUIContent(displayText, cachedBranchName), GetLabelStyle(textColor) ); EditorGUI.DrawRect(dotRect, indicatorColor); EditorGUIUtility.AddCursorRect(rect, MouseCursor.Link); if (Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition)) { ShowContextMenu(); Event.current.Use(); } } private static string GetShortBranchName(string branchName, GitState state) { if (state == GitState.DetachedHead) return "DETACHED"; if (state == GitState.NoRepository) return "No Repo"; if (state == GitState.Unknown) return "..."; if (string.IsNullOrWhiteSpace(branchName)) return "..."; const int maxLen = 18; if (branchName.Length <= maxLen) return branchName; return branchName.Substring(0, maxLen - 1) + "…"; } private static void DrawBackground(Rect rect, Color color) { var oldColor = GUI.color; GUI.color = color; GUI.Box(rect, GUIContent.none, EditorStyles.toolbarButton); GUI.color = oldColor; } private static GUIStyle GetLabelStyle(Color textColor) { var style = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft, fontStyle = FontStyle.Bold, clipping = TextClipping.Clip, padding = new RectOffset(0, 0, 0, 0), margin = new RectOffset(0, 0, 0, 0), fixedHeight = (int)Height }; style.normal.textColor = textColor; style.hover.textColor = textColor; style.active.textColor = textColor; style.focused.textColor = textColor; return style; } private static GUIContent GetDisplayContent() { return cachedState switch { GitState.Branch => new GUIContent($"{cachedBranchName + "TEST TEST TEST"}", cachedBranchName), GitState.DetachedHead => new GUIContent("DETACHED"), GitState.NoRepository => new GUIContent("No Repo"), _ => new GUIContent("...") }; } private static Color GetBackgroundColor(GitState state, string branchName) { string normalized = (branchName ?? string.Empty).Trim(); return state switch { GitState.NoRepository => new Color(0.25f, 0.25f, 0.25f, 1f), GitState.DetachedHead => new Color(0.45f, 0.30f, 0.10f, 1f), GitState.Branch when normalized.Equals("main", StringComparison.OrdinalIgnoreCase) => Color.red, GitState.Branch when normalized.Equals("develop", StringComparison.OrdinalIgnoreCase) => Color.blue, GitState.Branch when normalized.StartsWith("feature-", StringComparison.OrdinalIgnoreCase) => Color.green, GitState.Branch when normalized.StartsWith("feature/", StringComparison.OrdinalIgnoreCase) => new Color(0.12f, 0.28f, 0.45f, 1f), GitState.Branch when normalized.StartsWith("hotfix-", StringComparison.OrdinalIgnoreCase) => Color.yellow, GitState.Branch when normalized.StartsWith("hotfix/", StringComparison.OrdinalIgnoreCase) => new Color(0.45f, 0.20f, 0.10f, 1f), GitState.Branch when normalized.StartsWith("bugfix-", StringComparison.OrdinalIgnoreCase) => Color.gray, GitState.Branch => new Color(0.18f, 0.35f, 0.22f, 1f), _ => new Color(0.22f, 0.22f, 0.22f, 1f) }; } private static Color GetTextColor(GitState state) { return state switch { GitState.NoRepository => new Color(0.85f, 0.85f, 0.85f), _ => Color.white }; } private static void ShowContextMenu() { var menu = new GenericMenu(); menu.AddDisabledItem(new GUIContent($"Branch: {cachedBranchName}")); if (cachedState == GitState.Branch || cachedState == GitState.DetachedHead) { menu.AddItem(new GUIContent("Copy Branch Name"), false, () => { EditorGUIUtility.systemCopyBuffer = cachedBranchName; Debug.Log($"Copied Git branch name: {cachedBranchName}"); }); } else { menu.AddDisabledItem(new GUIContent("Copy Branch Name")); } menu.AddSeparator(""); menu.AddItem(new GUIContent("Refresh"), false, () => { RefreshBranchInfo(force: true); }); menu.ShowAsContext(); } private static void RefreshBranchInfo(bool force = false) { if (!force && EditorApplication.timeSinceStartup < nextRefreshTime) return; nextRefreshTime = EditorApplication.timeSinceStartup + RefreshInterval; try { string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); string gitDir = FindGitDirectory(projectRoot); if (string.IsNullOrEmpty(gitDir)) { cachedBranchName = "No Git"; cachedState = GitState.NoRepository; return; } string headPath = Path.Combine(gitDir, "HEAD"); if (!File.Exists(headPath)) { cachedBranchName = "No Git"; cachedState = GitState.NoRepository; return; } string headContent = File.ReadAllText(headPath).Trim(); if (headContent.StartsWith("ref:", StringComparison.OrdinalIgnoreCase)) { const string headsPrefix = "refs/heads/"; int headsIndex = headContent.IndexOf(headsPrefix, StringComparison.OrdinalIgnoreCase); if (headsIndex >= 0) { cachedBranchName = headContent.Substring(headsIndex + headsPrefix.Length).Trim(); } else { cachedBranchName = headContent .Replace("ref:", string.Empty) .Trim() .Split('/') .LastOrDefault() ?? "unknown"; } cachedState = GitState.Branch; } else { cachedBranchName = "DETACHED"; cachedState = GitState.DetachedHead; } } catch { cachedBranchName = "Git ?"; cachedState = GitState.Unknown; } } private static string FindGitDirectory(string startDirectory) { var dir = new DirectoryInfo(startDirectory); while (dir != null) { string gitPath = Path.Combine(dir.FullName, ".git"); if (Directory.Exists(gitPath)) return gitPath; if (File.Exists(gitPath)) { string content = File.ReadAllText(gitPath).Trim(); const string prefix = "gitdir:"; if (content.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { string relativePath = content.Substring(prefix.Length).Trim(); string resolvedPath = Path.GetFullPath(Path.Combine(dir.FullName, relativePath)); if (Directory.Exists(resolvedPath)) return resolvedPath; } } dir = dir.Parent; } return null; } } #endif