GitMergeWindow.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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) || assetType == typeof(BrokenPrefabAsset) ||
  220. assetType == typeof(DefaultAsset);
  221. }
  222. private static bool IsSceneAsset(Object asset)
  223. {
  224. var assetType = asset.GetType();
  225. return assetType == typeof(SceneAsset);
  226. }
  227. private static string PathDetectingDragAndDropField(string text, float height)
  228. {
  229. var currentEvent = Event.current;
  230. using (new GUIBackgroundColor(Color.black))
  231. {
  232. // Caching these sounds good on paper, but Unity tends to forget them randomly
  233. var content = EditorGUIUtility.IconContent("RectMask2D Icon", string.Empty);
  234. content.text = text;
  235. var buttonStyle = GUI.skin.GetStyle("Button");
  236. var style = new GUIStyle(GUI.skin.GetStyle("Box"));
  237. style.stretchWidth = true;
  238. style.normal.background = buttonStyle.normal.background;
  239. style.normal.textColor = buttonStyle.normal.textColor;
  240. style.alignment = TextAnchor.MiddleCenter;
  241. style.imagePosition = ImagePosition.ImageAbove;
  242. GUILayout.Box(content, style, GUILayout.Height(height));
  243. }
  244. var rect = GUILayoutUtility.GetLastRect();
  245. if (rect.Contains(currentEvent.mousePosition))
  246. {
  247. if (DragAndDrop.objectReferences.Length == 1)
  248. {
  249. switch (currentEvent.type)
  250. {
  251. case EventType.DragUpdated:
  252. var asset = DragAndDrop.objectReferences[0];
  253. if (IsPrefabAsset(asset) || IsSceneAsset(asset))
  254. {
  255. DragAndDrop.visualMode = DragAndDropVisualMode.Move;
  256. }
  257. break;
  258. case EventType.DragPerform:
  259. var path = AssetDatabase.GetAssetPath(DragAndDrop.objectReferences[0]);
  260. DragAndDrop.AcceptDrag();
  261. return path;
  262. }
  263. }
  264. }
  265. return null;
  266. }
  267. /// <summary>
  268. /// Tab that offers various settings for the tool.
  269. /// </summary>
  270. private void OnGUISettingsTab()
  271. {
  272. var vcsPath = vcs.GetExePath();
  273. var vcsPathNew = EditorGUILayout.TextField("Path to git.exe", vcsPath);
  274. if (vcsPath != vcsPathNew)
  275. {
  276. vcs.SetPath(vcsPathNew);
  277. }
  278. automerge = DisplaySettingsToggle(automerge,
  279. EDITOR_PREFS_AUTOMERGE,
  280. "Automerge",
  281. "(Automerge new/deleted GameObjects/Components upon merge start)");
  282. autofocus = DisplaySettingsToggle(autofocus,
  283. EDITOR_PREFS_AUTOFOCUS,
  284. "Auto Highlight",
  285. "(Highlight GameObjects when applying a MergeAction to it)");
  286. }
  287. private static bool DisplaySettingsToggle(bool value, string editorPrefsKey, string title, string description)
  288. {
  289. var newValue = EditorGUILayout.Toggle(title, value);
  290. if (value != newValue)
  291. {
  292. EditorPrefs.SetBool(editorPrefsKey, value);
  293. }
  294. GUILayout.Label(description);
  295. return newValue;
  296. }
  297. /// <summary>
  298. /// If no merge is in progress, draws the buttons to switch between tabs.
  299. /// Otherwise, draws the "abort merge" button.
  300. /// </summary>
  301. private void DrawTabButtons()
  302. {
  303. if (!mergeInProgress)
  304. {
  305. string[] tabs = { "Merge", "Settings" };
  306. tab = GUI.SelectionGrid(new Rect(72, 36, 300, 22), tab, tabs, 3);
  307. }
  308. else
  309. {
  310. GUI.backgroundColor = new Color(1, 0.4f, 0.4f, 1);
  311. if (GUI.Button(new Rect(72, 36, 300, 22), "Abort merge"))
  312. {
  313. mergeManager.AbortMerge();
  314. mergeManager = null;
  315. }
  316. // Confirm merge if possible
  317. if (mergeManager != null && mergeManager.isMergingDone)
  318. {
  319. GUI.backgroundColor = Color.green;
  320. if (GUI.Button(new Rect(400, 36, 300, 22), "Confirm merge"))
  321. {
  322. mergeManager.CompleteMerge();
  323. mergeManager = null;
  324. }
  325. }
  326. GUI.backgroundColor = Color.white;
  327. }
  328. }
  329. private void DrawHorizontalDivider()
  330. {
  331. var rect = EditorGUILayout.GetControlRect(false, 1);
  332. EditorGUI.DrawRect(rect, new Color(0.4f, 0.4f, 0.4f, 1f)); // Dark gray line
  333. }
  334. /// <summary>
  335. /// Displays all MergeActions and the "apply merge" button if a merge is in progress.
  336. /// </summary>
  337. private void DisplayMergeProcess()
  338. {
  339. DrawCommandBar();
  340. EditorGUILayout.Space();
  341. DrawHorizontalDivider();
  342. var done = DisplayMergeActions();
  343. // GUILayout.BeginHorizontal();
  344. // if (done && GUILayout.Button("Apply merge", GUILayout.Height(40)))
  345. // {
  346. // mergeManager.CompleteMerge();
  347. // mergeManager = null;
  348. // }
  349. // GUILayout.EndHorizontal();
  350. }
  351. /// <summary>
  352. /// Display extra commands to simplify merge process
  353. /// </summary>
  354. private void DrawCommandBar()
  355. {
  356. DrawQuickMergeSideSelectionCommands();
  357. filterBar.Draw();
  358. }
  359. /// <summary>
  360. /// Allow to select easily 'use ours' or 'use theirs' for all actions
  361. /// </summary>
  362. private void DrawQuickMergeSideSelectionCommands()
  363. {
  364. GUILayout.BeginHorizontal();
  365. {
  366. if (GUILayout.Button(new GUIContent("Use ours", "Use theirs for all. Do not apply merge automatically.")))
  367. {
  368. mergeManager.allMergeActions.ForEach((action) =>
  369. {
  370. action.UseOurs(true);
  371. });
  372. }
  373. if (GUILayout.Button(new GUIContent("Use theirs", "Use theirs for all. Do not apply merge automatically.")))
  374. {
  375. mergeManager.allMergeActions.ForEach((action) =>
  376. {
  377. action.UseTheirs(true);
  378. });
  379. }
  380. GUILayout.FlexibleSpace();
  381. }
  382. GUILayout.EndHorizontal();
  383. }
  384. /// <summary>
  385. /// Displays all GameObjectMergeActions.
  386. /// </summary>
  387. /// <returns>True, if all MergeActions are flagged as "merged".</returns>
  388. private bool DisplayMergeActions()
  389. {
  390. var textColor = GUI.skin.label.normal.textColor;
  391. GUI.skin.label.normal.textColor = Color.black;
  392. bool done = true;
  393. pageView.Draw(mergeActionsFiltered.Count, (index) =>
  394. {
  395. var actions = mergeActionsFiltered[index];
  396. actions.OnGUI();
  397. done = done && actions.merged;
  398. });
  399. GUI.skin.label.normal.textColor = textColor;
  400. return done;
  401. }
  402. private void CacheMergeActions()
  403. {
  404. if (filter.useFilter)
  405. {
  406. mergeActionsFiltered = mergeManager.allMergeActions.Where((actions) => filter.IsPassingFilter(actions)).ToList();
  407. }
  408. else
  409. {
  410. mergeActionsFiltered = mergeManager.allMergeActions;
  411. }
  412. mergeActionsFiltered = mergeManager.allMergeActions.Where((actions) => !actions.applied).ToList();
  413. }
  414. }
  415. }