GitMergeWindow.cs 16 KB

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