GitMergeWindow.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. using System;
  2. using UnityEngine;
  3. using UnityEditor;
  4. using UnityEngine.SceneManagement;
  5. using System.Linq;
  6. using System.Collections.Generic;
  7. using GitMerge.Utilities;
  8. using Object = UnityEngine.Object;
  9. namespace GitMerge
  10. {
  11. /// <summary>
  12. /// The window that lets you perform merges on scenes and prefabs.
  13. /// </summary>
  14. public class GitMergeWindow : EditorWindow
  15. {
  16. private VCS vcs = new VCSGit();
  17. private const string EDITOR_PREFS_AUTOMERGE = "GitMerge_automerge";
  18. private const string EDITOR_PREFS_AUTOFOCUS = "GitMerge_autofocus";
  19. public static bool automerge { private set; get; }
  20. public static bool autofocus { private set; get; }
  21. private MergeManagerBase mergeManager;
  22. private MergeFilter filter = new MergeFilter();
  23. private MergeFilterBar filterBar = new MergeFilterBar();
  24. public bool mergeInProgress => mergeManager != null;
  25. private PageView pageView = new PageView();
  26. private Vector2 scrollPosition = Vector2.zero;
  27. private int tab = 0;
  28. private List<GameObjectMergeActions> mergeActionsFiltered;
  29. private Texture2D brokenLogo;
  30. private Texture2D fixedLogo;
  31. private const string DockNextTo = "UnityEditor.GameView,UnityEditor.dll";
  32. private static void OpenEditor()
  33. {
  34. GetWindow();
  35. }
  36. private static EditorWindow GetWindow()
  37. {
  38. var type = Type.GetType(DockNextTo);
  39. var window = GetWindow<GitMergeWindow>(type);
  40. window.titleContent = new GUIContent("Git Merge", EditorGUIUtility.IconContent("TreeEditor.Distribution On").image);
  41. window.autoRepaintOnSceneChange = true;
  42. return window;
  43. }
  44. public static void ResolveConflict(string path)
  45. {
  46. if (path.Contains(".unity") || path.Contains(".prefab"))
  47. {
  48. var mergeWindow = GetWindow() as GitMergeWindow;
  49. if (mergeWindow != null)
  50. {
  51. mergeWindow.InitializeMerge(path);
  52. }
  53. }
  54. throw new NotSupportedException($"Cannot resolve conflict for: {path}");
  55. }
  56. private void OnEnable()
  57. {
  58. brokenLogo = UnityEngine.Resources.Load<Texture2D>("chain-broken");
  59. fixedLogo = UnityEngine.Resources.Load<Texture2D>("check");
  60. pageView.NumElementsPerPage = 200;
  61. filterBar.filter = filter;
  62. filter.OnChanged += CacheMergeActions;
  63. Selection.selectionChanged += Repaint;
  64. EditorApplication.hierarchyWindowItemOnGUI += HighlightItems;
  65. LoadSettings();
  66. }
  67. private static void LoadSettings()
  68. {
  69. automerge = EditorPrefs.GetBool(EDITOR_PREFS_AUTOMERGE, true);
  70. autofocus = EditorPrefs.GetBool(EDITOR_PREFS_AUTOFOCUS, true);
  71. }
  72. void OnHierarchyChange()
  73. {
  74. // Repaint if we changed the scene
  75. this.Repaint();
  76. }
  77. // Always check for editor state changes, and abort the active merge process if needed
  78. private void Update()
  79. {
  80. if (MergeAction.inMergePhase &&
  81. (EditorApplication.isCompiling ||
  82. EditorApplication.isPlayingOrWillChangePlaymode))
  83. {
  84. ShowNotification(new GUIContent("Aborting merge due to editor state change."));
  85. AbortMerge(false);
  86. }
  87. }
  88. private void AbortMerge(bool showNotification = true)
  89. {
  90. mergeManager.AbortMerge(showNotification);
  91. mergeManager = null;
  92. }
  93. private void OnGUI()
  94. {
  95. Resources.DrawLogo();
  96. DrawTabButtons();
  97. EditorGUILayout.Space();
  98. DrawHorizontalDivider();
  99. EditorGUILayout.Space();
  100. switch (tab)
  101. {
  102. case 0:
  103. OnGUIStartMergeTab();
  104. break;
  105. default:
  106. OnGUISettingsTab();
  107. break;
  108. }
  109. }
  110. /// <summary>
  111. /// Tab that offers scene merging.
  112. /// </summary>
  113. private void OnGUIStartMergeTab()
  114. {
  115. if (!mergeInProgress)
  116. {
  117. DisplayPrefabMergeField();
  118. GUILayout.Space(20);
  119. DisplaySceneMergeButton();
  120. }
  121. else
  122. {
  123. DisplayMergeProcess();
  124. }
  125. }
  126. private void DisplaySceneMergeButton()
  127. {
  128. var activeScene = SceneManager.GetActiveScene();
  129. GUILayout.Label("Open Scene: " + activeScene.path);
  130. if (activeScene.path != "" &&
  131. !mergeInProgress &&
  132. GUILayout.Button("Start merging the open scene", GUILayout.Height(30)))
  133. {
  134. var manager = new MergeManagerScene(this, vcs);
  135. if (manager.TryInitializeMerge())
  136. {
  137. this.mergeManager = manager;
  138. CacheMergeActions();
  139. }
  140. }
  141. }
  142. private void DisplayPrefabMergeField()
  143. {
  144. if (!mergeInProgress)
  145. {
  146. var path = PathDetectingDragAndDropField("Drag a scene or prefab here to start merging", 80);
  147. if (path != null)
  148. {
  149. InitializeMerge(path);
  150. }
  151. }
  152. }
  153. private void HighlightItems(int instanceID, Rect selectionRect)
  154. {
  155. var target = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
  156. Texture2D drawableLogo;
  157. if (target == null)
  158. {
  159. return;
  160. }
  161. bool isResolved = false;
  162. bool found = false;
  163. if (mergeManager is { allMergeActions: not null })
  164. {
  165. ObjectID targetId = ObjectID.GetFor(target);
  166. foreach (var mergeAction in mergeManager.allMergeActions)
  167. {
  168. ObjectID actionId = ObjectID.GetFor(mergeAction.ours);
  169. if (targetId.id == actionId.id)
  170. {
  171. found = true;
  172. isResolved = mergeAction.merged;
  173. break;
  174. }
  175. }
  176. }
  177. if (!found) return;
  178. if (!isResolved)
  179. {
  180. drawableLogo = brokenLogo;
  181. }
  182. else
  183. {
  184. drawableLogo = fixedLogo;
  185. }
  186. var iconSize = 16;
  187. var iconRect = new Rect(
  188. selectionRect.xMax - iconSize - 2,
  189. selectionRect.y + (selectionRect.height - iconSize) / 2f,
  190. iconSize,
  191. iconSize
  192. );
  193. GUI.DrawTexture(iconRect, drawableLogo, ScaleMode.ScaleToFit);
  194. }
  195. private void InitializeMerge(string path)
  196. {
  197. if (path == null) return;
  198. var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
  199. if (IsPrefabAsset(asset))
  200. {
  201. var manager = new MergeManagerPrefab(this, vcs);
  202. if (manager.TryInitializeMerge(path))
  203. {
  204. this.mergeManager = manager;
  205. CacheMergeActions();
  206. }
  207. }
  208. else if (IsSceneAsset(asset))
  209. {
  210. var manager = new MergeManagerScene(this, vcs);
  211. if (manager.TryInitializeMerge(path))
  212. {
  213. this.mergeManager = manager;
  214. CacheMergeActions();
  215. }
  216. }
  217. }
  218. private static bool IsPrefabAsset(Object asset)
  219. {
  220. var assetType = asset.GetType();
  221. return assetType == typeof(GameObject) ||
  222. // #if UNITY_2022_3_OR_NEWER
  223. // assetType == typeof(BrokenPrefabAsset) ||
  224. // #endif
  225. assetType == typeof(DefaultAsset);
  226. }
  227. private static bool IsSceneAsset(Object asset)
  228. {
  229. var assetType = asset.GetType();
  230. return assetType == typeof(SceneAsset);
  231. }
  232. private static string PathDetectingDragAndDropField(string text, float height)
  233. {
  234. var currentEvent = Event.current;
  235. using (new GUIBackgroundColor(Color.black))
  236. {
  237. // Caching these sounds good on paper, but Unity tends to forget them randomly
  238. var content = EditorGUIUtility.IconContent("RectMask2D Icon", string.Empty);
  239. content.text = text;
  240. var buttonStyle = GUI.skin.GetStyle("Button");
  241. var style = new GUIStyle(GUI.skin.GetStyle("Box"));
  242. style.stretchWidth = true;
  243. style.normal.background = buttonStyle.normal.background;
  244. style.normal.textColor = buttonStyle.normal.textColor;
  245. style.alignment = TextAnchor.MiddleCenter;
  246. style.imagePosition = ImagePosition.ImageAbove;
  247. GUILayout.Box(content, style, GUILayout.Height(height));
  248. }
  249. var rect = GUILayoutUtility.GetLastRect();
  250. if (rect.Contains(currentEvent.mousePosition))
  251. {
  252. if (DragAndDrop.objectReferences.Length == 1)
  253. {
  254. switch (currentEvent.type)
  255. {
  256. case EventType.DragUpdated:
  257. var asset = DragAndDrop.objectReferences[0];
  258. if (IsPrefabAsset(asset) || IsSceneAsset(asset))
  259. {
  260. DragAndDrop.visualMode = DragAndDropVisualMode.Move;
  261. }
  262. break;
  263. case EventType.DragPerform:
  264. var path = AssetDatabase.GetAssetPath(DragAndDrop.objectReferences[0]);
  265. DragAndDrop.AcceptDrag();
  266. return path;
  267. }
  268. }
  269. }
  270. return null;
  271. }
  272. /// <summary>
  273. /// Tab that offers various settings for the tool.
  274. /// </summary>
  275. private void OnGUISettingsTab()
  276. {
  277. var vcsPath = vcs.GetExePath();
  278. var vcsPathNew = EditorGUILayout.TextField("Path to git.exe", vcsPath);
  279. if (vcsPath != vcsPathNew)
  280. {
  281. vcs.SetPath(vcsPathNew);
  282. }
  283. automerge = DisplaySettingsToggle(automerge,
  284. EDITOR_PREFS_AUTOMERGE,
  285. "Automerge",
  286. "(Automerge new/deleted GameObjects/Components upon merge start)");
  287. autofocus = DisplaySettingsToggle(autofocus,
  288. EDITOR_PREFS_AUTOFOCUS,
  289. "Auto Highlight",
  290. "(Highlight GameObjects when applying a MergeAction to it)");
  291. }
  292. private static bool DisplaySettingsToggle(bool value, string editorPrefsKey, string title, string description)
  293. {
  294. var newValue = EditorGUILayout.Toggle(title, value);
  295. if (value != newValue)
  296. {
  297. EditorPrefs.SetBool(editorPrefsKey, value);
  298. }
  299. GUILayout.Label(description);
  300. return newValue;
  301. }
  302. /// <summary>
  303. /// If no merge is in progress, draws the buttons to switch between tabs.
  304. /// Otherwise, draws the "abort merge" button.
  305. /// </summary>
  306. private void DrawTabButtons()
  307. {
  308. if (!mergeInProgress)
  309. {
  310. string[] tabs = { "Merge", "Settings" };
  311. tab = GUI.SelectionGrid(new Rect(72, 36, 300, 22), tab, tabs, 3);
  312. }
  313. else
  314. {
  315. GUI.backgroundColor = new Color(1, 0.4f, 0.4f, 1);
  316. if (GUI.Button(new Rect(72, 36, 300, 22), "Abort merge"))
  317. {
  318. mergeManager.AbortMerge();
  319. mergeManager = null;
  320. }
  321. // Confirm merge if possible
  322. if (mergeManager != null && mergeManager.isMergingDone)
  323. {
  324. GUI.backgroundColor = Color.green;
  325. if (GUI.Button(new Rect(400, 36, 300, 22), "Confirm merge"))
  326. {
  327. mergeManager.CompleteMerge();
  328. mergeManager = null;
  329. }
  330. }
  331. GUI.backgroundColor = Color.white;
  332. }
  333. }
  334. private void DrawHorizontalDivider()
  335. {
  336. var rect = EditorGUILayout.GetControlRect(false, 1);
  337. EditorGUI.DrawRect(rect, new Color(0.4f, 0.4f, 0.4f, 1f)); // Dark gray line
  338. }
  339. /// <summary>
  340. /// Displays all MergeActions and the "apply merge" button if a merge is in progress.
  341. /// </summary>
  342. private void DisplayMergeProcess()
  343. {
  344. DrawCommandBar();
  345. EditorGUILayout.Space();
  346. DrawHorizontalDivider();
  347. var done = DisplayMergeActions();
  348. // GUILayout.BeginHorizontal();
  349. // if (done && GUILayout.Button("Apply merge", GUILayout.Height(40)))
  350. // {
  351. // mergeManager.CompleteMerge();
  352. // mergeManager = null;
  353. // }
  354. // GUILayout.EndHorizontal();
  355. }
  356. /// <summary>
  357. /// Display extra commands to simplify merge process
  358. /// </summary>
  359. private void DrawCommandBar()
  360. {
  361. DrawQuickMergeSideSelectionCommands();
  362. filterBar.Draw();
  363. }
  364. /// <summary>
  365. /// Allow to select easily 'use ours' or 'use theirs' for all actions
  366. /// </summary>
  367. private void DrawQuickMergeSideSelectionCommands()
  368. {
  369. GUILayout.BeginHorizontal();
  370. {
  371. if (GUILayout.Button(new GUIContent("Use ours", "Use theirs for all. Do not apply merge automatically.")))
  372. {
  373. mergeManager.allMergeActions.ForEach((action) =>
  374. {
  375. action.UseOurs(true);
  376. });
  377. }
  378. if (GUILayout.Button(new GUIContent("Use theirs", "Use theirs for all. Do not apply merge automatically.")))
  379. {
  380. mergeManager.allMergeActions.ForEach((action) =>
  381. {
  382. action.UseTheirs(true);
  383. });
  384. }
  385. GUILayout.FlexibleSpace();
  386. }
  387. GUILayout.EndHorizontal();
  388. }
  389. /// <summary>
  390. /// Displays all GameObjectMergeActions.
  391. /// </summary>
  392. /// <returns>True, if all MergeActions are flagged as "merged".</returns>
  393. private bool DisplayMergeActions()
  394. {
  395. var textColor = GUI.skin.label.normal.textColor;
  396. GUI.skin.label.normal.textColor = Color.black;
  397. bool done = true;
  398. pageView.Draw(mergeActionsFiltered.Count, (index) =>
  399. {
  400. var actions = mergeActionsFiltered[index];
  401. actions.OnGUI();
  402. done = done && actions.merged;
  403. });
  404. GUI.skin.label.normal.textColor = textColor;
  405. return done;
  406. }
  407. private void CacheMergeActions()
  408. {
  409. if (filter.useFilter)
  410. {
  411. mergeActionsFiltered = mergeManager.allMergeActions.Where((actions) => filter.IsPassingFilter(actions)).ToList();
  412. }
  413. else
  414. {
  415. mergeActionsFiltered = mergeManager.allMergeActions;
  416. }
  417. mergeActionsFiltered = mergeManager.allMergeActions.Where((actions) => !actions.applied).ToList();
  418. }
  419. }
  420. }