GitMergeWindow.cs 16 KB

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