411 lines
12 KiB
C#
411 lines
12 KiB
C#
#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<IMGUIContainer>(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 |