├── LICENSE.md.meta ├── README.md.meta ├── Editor ├── Resources │ ├── GitMergeBox.psd │ ├── GitMergeLogo.psd │ ├── Licenses.txt.meta │ ├── GitMergeStyles.asset.meta │ ├── Licenses.txt │ ├── GitMergeBox.psd.meta │ ├── GitMergeLogo.psd.meta │ └── GitMergeStyles.asset ├── Resources.meta ├── VCS.meta ├── MergeActions.meta ├── Utilities.meta ├── VCS │ ├── VCS.cs.meta │ ├── VCSGit.cs.meta │ ├── VCSException.cs.meta │ ├── VCSException.cs │ ├── VCSGit.cs │ └── VCS.cs ├── Resources.cs.meta ├── ThirteenPixels.GitMerge.asmdef.meta ├── GitMergeWindow.cs.meta ├── MergeManagerBase.cs.meta ├── MergeManagerPrefab.cs.meta ├── MergeManagerScene.cs.meta ├── ObjectDictionaries.cs.meta ├── GameObjectMergeActions.cs.meta ├── MergeActions │ ├── MergeAction.cs.meta │ ├── MergeActionExistence.cs.meta │ ├── MergeActionParenting.cs.meta │ ├── MergeActionChangeValues.cs.meta │ ├── MergeActionNewComponent.cs.meta │ ├── MergeActionNewGameObject.cs.meta │ ├── MergeActionDeleteComponent.cs.meta │ ├── MergeActionDeleteGameObject.cs.meta │ ├── MergeActionExistence.cs │ ├── MergeActionDeleteGameObject.cs │ ├── MergeActionNewGameObject.cs │ ├── MergeActionNewComponent.cs │ ├── MergeActionDeleteComponent.cs │ ├── MergeActionParenting.cs │ ├── MergeAction.cs │ └── MergeActionChangeValues.cs ├── Utilities │ ├── ObjectExtensions.cs.meta │ ├── GameObjectExtensions.cs.meta │ ├── SerializedPropertyExtensions.cs.meta │ ├── ObjectID.cs.meta │ ├── PageView.cs.meta │ ├── MergeFilter.cs.meta │ ├── MergeFilterBar.cs.meta │ ├── GUIBackgroundColor.cs.meta │ ├── GUIBackgroundColor.cs │ ├── ObjectExtensions.cs │ ├── MergeFilterBar.cs │ ├── ObjectID.cs │ ├── PageView.cs │ ├── GameObjectExtensions.cs │ ├── MergeFilter.cs │ └── SerializedPropertyExtensions.cs ├── ThirteenPixels.GitMerge.asmdef ├── Resources.cs ├── MergeManagerBase.cs ├── MergeManagerScene.cs ├── MergeManagerPrefab.cs ├── ObjectDictionaries.cs ├── GameObjectMergeActions.cs └── GitMergeWindow.cs ├── Editor.meta ├── CHANGELOG.md.meta ├── package.json.meta ├── CHANGELOG.md ├── README.md ├── package.json └── LICENSE.md /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 785798e4822ed284b8daad687e2df88d 3 | DefaultImporter: 4 | userData: 5 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b364bc2946220bf428dccb47dbcee795 3 | DefaultImporter: 4 | userData: 5 | -------------------------------------------------------------------------------- /Editor/Resources/GitMergeBox.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlaShG/GitMerge-for-Unity/HEAD/Editor/Resources/GitMergeBox.psd -------------------------------------------------------------------------------- /Editor/Resources/GitMergeLogo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlaShG/GitMerge-for-Unity/HEAD/Editor/Resources/GitMergeLogo.psd -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 13c149b5679992c4aad3260153c5f7ae 3 | folderAsset: yes 4 | DefaultImporter: 5 | userData: 6 | -------------------------------------------------------------------------------- /Editor/Resources/Licenses.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fcbbf2375e3889c4398c97d289eab2ac 3 | TextScriptImporter: 4 | userData: 5 | -------------------------------------------------------------------------------- /Editor/Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 718f5b21123c36448b66c0e2ec52031c 3 | folderAsset: yes 4 | DefaultImporter: 5 | userData: 6 | -------------------------------------------------------------------------------- /Editor/Resources/GitMergeStyles.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a88667aa2092d4b46aa3ad92e778e994 3 | NativeFormatImporter: 4 | userData: 5 | -------------------------------------------------------------------------------- /Editor/Resources/Licenses.txt: -------------------------------------------------------------------------------- 1 | Git Logo by Jason Long [used in GitMergeLogo.psd] is licensed under the Creative Commons Attribution 3.0 Unported License. -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4e6e06c50abd73548b1cb0e188e376f6 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 90678a57e53d4b44cb92ee6041bd5a1b 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/VCS.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 69dff03f1fb53f248beefa85b552d2a1 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.1.0] - 2020-04-23 2 | - Add package.json and changelog. 3 | - The tool is in a halfway working state since development was forced to be on hold for five years. Thus, the version number starts at v0.1.0. 4 | -------------------------------------------------------------------------------- /Editor/MergeActions.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c07bea28e6453b245a26dd5bb2a75d1a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Utilities.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 04df0253576a3944ea3e76382e336da9 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/VCS/VCS.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a08026e2ac14c0446a85134863995fd0 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/Resources.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 67422f4e577bb17409644dcfe6bf75c8 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/ThirteenPixels.GitMerge.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 13e5a9e99b20be54998e95b60302a730 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/VCS/VCSGit.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b4b1187c33991394a9c66710b250f24b 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/GitMergeWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aaa504546605b3a479405bee3c11cd04 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeManagerBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b7759967878514e4aace05e726704e8f 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeManagerPrefab.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f8db1a549c01168468790b325c85f3c1 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeManagerScene.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2143b0a739488bb4698c51777c4863fc 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/ObjectDictionaries.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e2d4344037c6c7c4ca962a1e4037b6fe 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/VCS/VCSException.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 339de811f43309449914e314f1366850 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/GameObjectMergeActions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cfa76c0e71adc0f439476432ffe37fa3 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeAction.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d9583c639208c1e49b6cc77e128f7ccb 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/Utilities/ObjectExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0ee0303a3ea013b4eaa610e764182b3a 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionExistence.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f94b44d1572d0d14ea949190ef678a3a 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionParenting.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 29139dc665a42db45b67df932a04c810 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/Utilities/GameObjectExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 327c0f7ed6330514791026c2d1ba5a5c 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionChangeValues.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9a327ed62a5572a459e54869df134f42 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionNewComponent.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6c13706c0946b934a95c41957ce759f5 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionNewGameObject.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fb60feef4712bcc4a9f2d266c289a2d7 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/Utilities/SerializedPropertyExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e055732d013ee174fb764659113f8ea9 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionDeleteComponent.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6414ac2f8bebc8b4bb33597977332630 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionDeleteGameObject.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 627176a0f25069f46919f6e3489eb8c3 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Editor/VCS/VCSException.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | public class VCSException : System.Exception 5 | { 6 | public VCSException(string message = "Could not find the VCS executable. Please enter the path to your VCS in the settings.") : base(message) 7 | { 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /Editor/Utilities/ObjectID.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f3b42f26cd5372b42982f0590353b893 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Utilities/PageView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e670dcb0849e9cc4292d2d2e86aee42c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Utilities/MergeFilter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: edab603871662724cb02582fea7689be 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Utilities/MergeFilterBar.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2342757c23c8b444d9923edcc54ada06 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Utilities/GUIBackgroundColor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6987b8c1c496fea44b2d36b5e09e167c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitMerge-for-Unity 2 | ================== 3 | 4 | A Unity plugin which allows you to merge scene and prefab files when using git. 5 | More information: http://flashg.github.io/GitMerge-for-Unity/ 6 | 7 | ## Current project state 8 | 9 | **IMPORTANT**: I've created a new version of this project that will replace this one when it's ready. There are still some edge cases to get working properly, but it will be linked here soon™. 10 | -------------------------------------------------------------------------------- /Editor/ThirteenPixels.GitMerge.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ThirteenPixels.GitMerge", 3 | "references": [], 4 | "includePlatforms": [ 5 | "Editor" 6 | ], 7 | "excludePlatforms": [], 8 | "allowUnsafeCode": false, 9 | "overrideReferences": false, 10 | "precompiledReferences": [], 11 | "autoReferenced": true, 12 | "defineConstraints": [], 13 | "versionDefines": [], 14 | "noEngineReferences": false 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "de.thirteenpixels.gitmerge", 3 | "version": "0.1.0", 4 | "displayName": "GitMerge for Unity", 5 | "description": "Allows merging scene, prefab and ScriptableObject files.", 6 | "unity": "2019.2", 7 | "dependencies": { 8 | }, 9 | "keywords": [ 10 | "VCS", 11 | "Git", 12 | "Merging" 13 | ], 14 | "author": { 15 | "name": "13Pixels", 16 | "email": "assetstore@13pixels.de", 17 | "url": "https://www.13pixels.de" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Editor/Utilities/GUIBackgroundColor.cs: -------------------------------------------------------------------------------- 1 | 2 | using UnityEngine; 3 | using System; 4 | 5 | namespace GitMerge 6 | { 7 | #if CSHARP_7_3_OR_NEWER 8 | public readonly struct GUIBackgroundColor : IDisposable 9 | #else 10 | public struct GUIBackgroundColor : IDisposable 11 | #endif 12 | { 13 | private readonly Color previousColor; 14 | 15 | public GUIBackgroundColor(Color color) 16 | { 17 | previousColor = GUI.backgroundColor; 18 | GUI.backgroundColor = color; 19 | } 20 | 21 | public void Dispose() 22 | { 23 | GUI.backgroundColor = previousColor; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Editor/Utilities/ObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace GitMerge 4 | { 5 | public static class ObjectExtensions 6 | { 7 | /// 8 | /// Get a fine, readable type string. Doesn't really need to be a Component extension method. 9 | /// Example: UnityEngine.BoxCollider => BoxCollider 10 | /// 11 | /// The object whose type we want to display 12 | /// The well readable type string 13 | public static string GetPlainType(this object o) 14 | { 15 | var s = o.GetType().ToString(); 16 | var i = s.LastIndexOf('.'); 17 | if (i >= 0) 18 | { 19 | s = s.Substring(i + 1); 20 | } 21 | return s; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionExistence.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | 6 | /// 7 | /// The abstract base MergeAction for all MergeActions that manage whether or not an object exists 8 | /// 9 | public abstract class MergeActionExistence : MergeAction 10 | { 11 | public MergeActionExistence(GameObject ours, GameObject theirs) 12 | : base(ours, theirs) 13 | { 14 | ObjectDictionaries.AddToSchroedingersObjects(ours ?? theirs, this); 15 | } 16 | 17 | /// 18 | /// Apply whatever version that has the object existing, since it might be needed somewhere. 19 | /// When overriding, call either UseOurs or UseTheirs to make sure to trigger the side effects. 20 | /// 21 | public abstract void EnsureExistence(); 22 | } 23 | } -------------------------------------------------------------------------------- /Editor/Resources/GitMergeBox.psd.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: be9ca0515e031fb4c86b12a6b9ba2410 3 | TextureImporter: 4 | serializedVersion: 2 5 | mipmaps: 6 | mipMapMode: 0 7 | enableMipMap: 0 8 | linearTexture: 1 9 | correctGamma: 0 10 | fadeOut: 0 11 | borderMipMap: 0 12 | mipMapFadeDistanceStart: 1 13 | mipMapFadeDistanceEnd: 3 14 | bumpmap: 15 | convertToNormalMap: 0 16 | externalNormalMap: 0 17 | heightScale: .25 18 | normalMapFilter: 0 19 | isReadable: 0 20 | grayScaleToAlpha: 0 21 | generateCubemap: 0 22 | seamlessCubemap: 0 23 | textureFormat: -1 24 | maxTextureSize: 1024 25 | textureSettings: 26 | filterMode: -1 27 | aniso: 1 28 | mipBias: -1 29 | wrapMode: 1 30 | nPOTScale: 0 31 | lightmap: 0 32 | compressionQuality: 50 33 | spriteMode: 0 34 | spriteExtrude: 1 35 | spriteMeshType: 1 36 | alignment: 0 37 | spritePivot: {x: .5, y: .5} 38 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 39 | spritePixelsToUnits: 100 40 | alphaIsTransparency: 1 41 | textureType: 2 42 | buildTargetSettings: [] 43 | spriteSheet: 44 | sprites: [] 45 | spritePackingTag: 46 | userData: 47 | -------------------------------------------------------------------------------- /Editor/Resources/GitMergeLogo.psd.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e18ddd74b76685d46a46ca56a3f6380f 3 | TextureImporter: 4 | serializedVersion: 2 5 | mipmaps: 6 | mipMapMode: 0 7 | enableMipMap: 0 8 | linearTexture: 1 9 | correctGamma: 0 10 | fadeOut: 0 11 | borderMipMap: 0 12 | mipMapFadeDistanceStart: 1 13 | mipMapFadeDistanceEnd: 3 14 | bumpmap: 15 | convertToNormalMap: 0 16 | externalNormalMap: 0 17 | heightScale: .25 18 | normalMapFilter: 0 19 | isReadable: 0 20 | grayScaleToAlpha: 0 21 | generateCubemap: 0 22 | seamlessCubemap: 0 23 | textureFormat: -1 24 | maxTextureSize: 1024 25 | textureSettings: 26 | filterMode: -1 27 | aniso: 1 28 | mipBias: -1 29 | wrapMode: 1 30 | nPOTScale: 0 31 | lightmap: 0 32 | compressionQuality: 50 33 | spriteMode: 0 34 | spriteExtrude: 1 35 | spriteMeshType: 1 36 | alignment: 0 37 | spritePivot: {x: .5, y: .5} 38 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 39 | spritePixelsToUnits: 100 40 | alphaIsTransparency: 1 41 | textureType: 2 42 | buildTargetSettings: [] 43 | spriteSheet: 44 | sprites: [] 45 | spritePackingTag: 46 | userData: 47 | -------------------------------------------------------------------------------- /Editor/Resources.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace GitMerge 4 | { 5 | public class Resources : ScriptableObject 6 | { 7 | private static Resources _styles; 8 | public static Resources styles 9 | { 10 | get 11 | { 12 | if (!_styles) 13 | { 14 | _styles = UnityEngine.Resources.Load("GitMergeStyles"); 15 | } 16 | return _styles; 17 | } 18 | } 19 | private static Texture2D _logo; 20 | public static Texture2D logo 21 | { 22 | get 23 | { 24 | if (!_logo) 25 | { 26 | _logo = UnityEngine.Resources.Load("GitMergeLogo"); 27 | } 28 | return _logo; 29 | } 30 | } 31 | 32 | public GUIStyle mergeActions; 33 | public GUIStyle mergeAction; 34 | 35 | public static void DrawLogo() 36 | { 37 | GUI.DrawTexture(new Rect(5, 5, logo.width, logo.height), logo); 38 | GUILayout.Space(logo.height + 15); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Editor/VCS/VCSGit.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | 6 | public class VCSGit : VCS 7 | { 8 | protected override string GetDefaultPath() 9 | { 10 | if (Application.platform == RuntimePlatform.WindowsEditor) 11 | { 12 | return @"C:\Program Files (x86)\Git\bin\git.exe"; 13 | } 14 | return @"/usr/bin/git"; 15 | } 16 | 17 | protected override string EditorPrefsKey() 18 | { 19 | return "GitMerge_git"; 20 | } 21 | 22 | public override void CheckoutOurs(string path) 23 | { 24 | GetAbsoluteFolderPathAndFilename(path, out var absoluteFolderPath, out var filename); 25 | Execute("checkout --ours \"" + filename + "\"", absoluteFolderPath); 26 | } 27 | 28 | public override void CheckoutTheirs(string path) 29 | { 30 | GetAbsoluteFolderPathAndFilename(path, out var absoluteFolderPath, out var filename); 31 | Execute("checkout --theirs \"" + filename + "\"", absoluteFolderPath); 32 | } 33 | 34 | public override void MarkAsMerged(string path) 35 | { 36 | GetAbsoluteFolderPathAndFilename(path, out var absoluteFolderPath, out var filename); 37 | Execute("add \"" + filename + "\"", absoluteFolderPath); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Editor/Utilities/MergeFilterBar.cs: -------------------------------------------------------------------------------- 1 | using GitMerge; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEditor; 5 | using UnityEditor.AnimatedValues; 6 | using UnityEngine; 7 | 8 | public class MergeFilterBar 9 | { 10 | public MergeFilter filter { get; set; } 11 | 12 | private AnimBool filterAnimAlpha; 13 | 14 | public void Draw() 15 | { 16 | filter.useFilter = GUILayout.Toggle(filter.useFilter, "Filter"); 17 | if (filter.useFilter) 18 | { 19 | using (new EditorGUI.IndentLevelScope()) 20 | { 21 | filter.isRegex = EditorGUILayout.Toggle("Regex?", filter.isRegex); 22 | if (!filter.isRegex) 23 | { 24 | filter.isCaseSensitive = EditorGUILayout.Toggle("Case Sensitive", filter.isCaseSensitive); 25 | } 26 | filter.expression = EditorGUILayout.TextField("Expression", filter.expression, GUILayout.ExpandWidth(true)); 27 | 28 | filter.filterMode = (MergeFilter.FilterMode)EditorGUILayout.EnumPopup("Mode", filter.filterMode, GUILayout.Width(300), GUILayout.ExpandWidth(false)); 29 | filter.filterState = (MergeFilter.FilterState)EditorGUILayout.EnumFlagsField("Conflict State", filter.filterState, GUILayout.Width(300), GUILayout.ExpandWidth(false)); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionDeleteGameObject.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace GitMerge 5 | { 6 | /// 7 | /// The MergeAction that handles a GameObject which exists in "our" version but not "theirs". 8 | /// 9 | public class MergeActionDeleteGameObject : MergeActionExistence 10 | { 11 | private bool oursWasActive; 12 | 13 | public MergeActionDeleteGameObject(GameObject ours, GameObject theirs) 14 | : base(ours, theirs) 15 | { 16 | oursWasActive = ours.activeSelf; 17 | 18 | if (GitMergeWindow.automerge) 19 | { 20 | UseOurs(); 21 | } 22 | } 23 | 24 | protected override void ApplyOurs() 25 | { 26 | ours.SetActiveForMerging(true); 27 | ours.SetActive(oursWasActive); 28 | } 29 | 30 | protected override void ApplyTheirs() 31 | { 32 | ours.SetActiveForMerging(false); 33 | SceneView.RepaintAll(); 34 | } 35 | 36 | public override void EnsureExistence() 37 | { 38 | UseOurs(); 39 | } 40 | 41 | public override void OnGUI() 42 | { 43 | var defaultOptionColor = merged ? Color.gray : Color.white; 44 | 45 | GUI.color = usingOurs ? Color.green : defaultOptionColor; 46 | if (GUILayout.Button("Keep GameObject")) 47 | { 48 | UseOurs(); 49 | } 50 | GUI.color = usingTheirs ? Color.green : defaultOptionColor; 51 | if (GUILayout.Button("Delete GameObject")) 52 | { 53 | UseTheirs(); 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionNewGameObject.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace GitMerge 5 | { 6 | /// 7 | /// The MergeAction that handles GameObjects that exist in "their" version but not in "ours". 8 | /// 9 | public class MergeActionNewGameObject : MergeActionExistence 10 | { 11 | public MergeActionNewGameObject(GameObject ours, GameObject theirs) 12 | : base(ours, theirs) 13 | { 14 | if (GitMergeWindow.automerge) 15 | { 16 | UseTheirs(); 17 | } 18 | } 19 | 20 | protected override void ApplyOurs() 21 | { 22 | if (ours) 23 | { 24 | ObjectDictionaries.RemoveCopyOf(theirs); 25 | GameObject.DestroyImmediate(ours, true); 26 | } 27 | } 28 | 29 | protected override void ApplyTheirs() 30 | { 31 | if (!ours) 32 | { 33 | ours = ObjectDictionaries.InstantiateFromMerging(theirs); 34 | ObjectDictionaries.SetAsCopy(ours, theirs); 35 | } 36 | } 37 | 38 | public override void EnsureExistence() 39 | { 40 | UseTheirs(); 41 | } 42 | 43 | public override void OnGUI() 44 | { 45 | var defaultOptionColor = merged ? Color.gray : Color.white; 46 | 47 | GUI.color = usingOurs ? Color.green : defaultOptionColor; 48 | if (GUILayout.Button("Don't add GameObject")) 49 | { 50 | UseOurs(); 51 | } 52 | GUI.color = usingTheirs ? Color.green : defaultOptionColor; 53 | if (GUILayout.Button("Add new GameObject")) 54 | { 55 | UseTheirs(); 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Editor/Utilities/ObjectID.cs: -------------------------------------------------------------------------------- 1 | // #define DEBUG_IDS 2 | 3 | namespace GitMerge 4 | { 5 | using UnityEngine; 6 | using UnityEditor; 7 | 8 | /// 9 | /// Struct representing a GameObject ID. 10 | /// Similar to GlobalObjectID, but used for robustness against changes to Unity's API. 11 | /// 12 | public struct ObjectID 13 | { 14 | public readonly ulong id; 15 | public readonly ulong prefabId; 16 | 17 | public ObjectID(ulong id, ulong prefabId) 18 | { 19 | this.id = id; 20 | this.prefabId = prefabId; 21 | } 22 | 23 | public override bool Equals(object obj) 24 | { 25 | if (obj == null || GetType() != obj.GetType()) 26 | { 27 | return false; 28 | } 29 | 30 | var other = (ObjectID)obj; 31 | 32 | return id == other.id && prefabId == other.prefabId; 33 | } 34 | 35 | public override int GetHashCode() 36 | { 37 | return unchecked(id.GetHashCode() + prefabId.GetHashCode()); 38 | } 39 | 40 | public override string ToString() 41 | { 42 | return "[" + id + (prefabId != 0 ? "/" + prefabId : "") + "]"; 43 | } 44 | 45 | public static ObjectID GetFor(Object o) 46 | { 47 | var goid = GlobalObjectId.GetGlobalObjectIdSlow(o); 48 | var id = goid.targetObjectId; 49 | var prefabId = goid.targetPrefabId; 50 | return new ObjectID(id, prefabId); 51 | } 52 | 53 | #if DEBUG_IDS 54 | [MenuItem("Window/GitMerge Test ObjectID")] 55 | private static void Test() 56 | { 57 | // Debug.Log(GlobalObjectId.GetGlobalObjectIdSlow(Selection.activeGameObject)); 58 | Debug.Log(GetFor(Selection.activeGameObject)); 59 | } 60 | #endif 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionNewComponent.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace GitMerge 5 | { 6 | /// 7 | /// The MergeAction that handles Components that exist in "our" version but not in "theirs". 8 | /// 9 | public class MergeActionNewComponent : MergeActionExistence 10 | { 11 | protected Component ourComponent; 12 | protected Component theirComponent; 13 | 14 | public MergeActionNewComponent(GameObject ours, Component theirComponent) 15 | : base(ours, null) 16 | { 17 | this.theirComponent = theirComponent; 18 | 19 | if (GitMergeWindow.automerge) 20 | { 21 | UseOurs(); 22 | } 23 | } 24 | 25 | protected override void ApplyOurs() 26 | { 27 | if (ourComponent) 28 | { 29 | ObjectDictionaries.RemoveCopyOf(theirComponent); 30 | Object.DestroyImmediate(ourComponent, true); 31 | } 32 | } 33 | 34 | protected override void ApplyTheirs() 35 | { 36 | if (!ourComponent) 37 | { 38 | ourComponent = ours.AddComponent(theirComponent); 39 | ObjectDictionaries.SetAsCopy(ourComponent, theirComponent); 40 | } 41 | } 42 | 43 | public override void EnsureExistence() 44 | { 45 | UseTheirs(); 46 | } 47 | 48 | public override void OnGUI() 49 | { 50 | GUILayout.Label(theirComponent.GetPlainType()); 51 | 52 | var defaultOptionColor = merged ? Color.gray : Color.white; 53 | 54 | GUI.color = usingOurs ? Color.green : defaultOptionColor; 55 | if (GUILayout.Button("Don't add Component")) 56 | { 57 | UseOurs(); 58 | } 59 | GUI.color = usingTheirs ? Color.green : defaultOptionColor; 60 | if (GUILayout.Button("Add new Component")) 61 | { 62 | UseTheirs(); 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionDeleteComponent.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace GitMerge 5 | { 6 | /// 7 | /// The MergeAction that handles a Component which exists in "their" version but not "ours". 8 | /// 9 | public class MergeActionDeleteComponent : MergeActionExistence 10 | { 11 | protected Component ourComponent; 12 | protected Component copy; 13 | 14 | public MergeActionDeleteComponent(GameObject ours, Component ourComponent) 15 | : base(ours, null) 16 | { 17 | this.ourComponent = ourComponent; 18 | 19 | var go = new GameObject("GitMerge Object"); 20 | go.SetActiveForMerging(false); 21 | 22 | copy = go.AddComponent(ourComponent); 23 | 24 | if (GitMergeWindow.automerge) 25 | { 26 | UseOurs(); 27 | } 28 | } 29 | 30 | protected override void ApplyOurs() 31 | { 32 | if (ourComponent == null) 33 | { 34 | ourComponent = ours.AddComponent(copy); 35 | ObjectDictionaries.SetAsOurObject(ourComponent); 36 | } 37 | } 38 | 39 | protected override void ApplyTheirs() 40 | { 41 | if (ourComponent != null) 42 | { 43 | ObjectDictionaries.RemoveOurObject(ourComponent); 44 | Object.DestroyImmediate(ourComponent, true); 45 | } 46 | } 47 | 48 | public override void EnsureExistence() 49 | { 50 | UseOurs(); 51 | } 52 | 53 | public override void OnGUI() 54 | { 55 | GUILayout.Label(copy.GetPlainType()); 56 | 57 | var defaultOptionColor = merged ? Color.gray : Color.white; 58 | 59 | GUI.color = usingOurs ? Color.green : defaultOptionColor; 60 | if (GUILayout.Button("Keep Component")) 61 | { 62 | UseOurs(); 63 | } 64 | GUI.color = usingTheirs ? Color.green : defaultOptionColor; 65 | if (GUILayout.Button("Delete Component")) 66 | { 67 | UseTheirs(); 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Editor/VCS/VCS.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | using UnityEditor; 6 | using System.Diagnostics; 7 | using System.ComponentModel; 8 | using System.IO; 9 | 10 | /// 11 | /// This abstract class represents a vcs interface. 12 | /// It manages saving and retrieving the exe path from/to the EditorPrefs 13 | /// and offers a small set of actions using the vcs. 14 | /// 15 | public abstract class VCS 16 | { 17 | protected abstract string GetDefaultPath(); 18 | protected abstract string EditorPrefsKey(); 19 | 20 | public abstract void CheckoutOurs(string path); 21 | public abstract void CheckoutTheirs(string path); 22 | public abstract void MarkAsMerged(string path); 23 | 24 | public string GetExePath() 25 | { 26 | if (EditorPrefs.HasKey(EditorPrefsKey())) 27 | { 28 | return EditorPrefs.GetString(EditorPrefsKey()); 29 | } 30 | 31 | return GetDefaultPath(); 32 | } 33 | 34 | public void SetPath(string path) 35 | { 36 | EditorPrefs.SetString(EditorPrefsKey(), path); 37 | } 38 | 39 | /// 40 | /// Executes the VCS as a subprocess. 41 | /// 42 | /// The parameters passed. Like "status" for "git status". 43 | /// Whatever the call returns. 44 | protected string Execute(string args, string workingDirectoryPath) 45 | { 46 | var process = new Process(); 47 | var startInfo = new ProcessStartInfo(); 48 | startInfo.WindowStyle = ProcessWindowStyle.Hidden; 49 | startInfo.FileName = GetExePath(); 50 | startInfo.Arguments = args; 51 | startInfo.UseShellExecute = false; 52 | startInfo.RedirectStandardOutput = true; 53 | startInfo.WorkingDirectory = workingDirectoryPath; 54 | process.StartInfo = startInfo; 55 | 56 | try 57 | { 58 | process.Start(); 59 | } 60 | catch (Win32Exception) 61 | { 62 | throw new VCSException(); 63 | } 64 | 65 | string output = process.StandardOutput.ReadToEnd(); 66 | process.WaitForExit(); 67 | 68 | return output; 69 | } 70 | 71 | private static string GetAboluteFolderPath(string relativeFilePath) 72 | { 73 | var projectPath = Application.dataPath; 74 | projectPath = Directory.GetParent(projectPath).FullName; 75 | var fullPath = Path.Combine(projectPath, relativeFilePath); 76 | return Path.GetDirectoryName(fullPath); 77 | } 78 | 79 | protected static void GetAbsoluteFolderPathAndFilename(string relativeFilePath, out string absoluteFolderPath, out string filename) 80 | { 81 | absoluteFolderPath = GetAboluteFolderPath(relativeFilePath); 82 | filename = Path.GetFileName(relativeFilePath); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionParenting.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace GitMerge 5 | { 6 | /// 7 | /// The MergeAction that handles a differing parents for a Transform. 8 | /// 9 | public class MergeActionParenting : MergeAction 10 | { 11 | private Transform transform; 12 | private Transform ourParent; 13 | private Transform theirParent; 14 | 15 | public MergeActionParenting(Transform transform, Transform ourParent, Transform theirParent) 16 | : base(transform.gameObject, null) 17 | { 18 | this.transform = transform; 19 | this.ourParent = ourParent; 20 | this.theirParent = theirParent; 21 | } 22 | 23 | protected override void ApplyOurs() 24 | { 25 | transform.parent = ourParent; 26 | } 27 | 28 | protected override void ApplyTheirs() 29 | { 30 | var ourVersion = ObjectDictionaries.GetOurCounterpartFor(theirParent) as Transform; 31 | if (theirParent && !ourVersion) 32 | { 33 | if (EditorUtility.DisplayDialog("The chosen parent currently does not exist.", "Do you want do add it?", "Yes", "No")) 34 | { 35 | ObjectDictionaries.EnsureExistence(theirParent.gameObject); 36 | ourVersion = ObjectDictionaries.GetOurCounterpartFor(theirParent) as Transform; 37 | 38 | transform.parent = ourVersion; 39 | } 40 | else 41 | { 42 | throw new System.Exception("User Abort."); 43 | } 44 | } 45 | else 46 | { 47 | transform.parent = ourVersion; 48 | } 49 | } 50 | 51 | public override void OnGUI() 52 | { 53 | GUILayout.BeginVertical(); 54 | GUILayout.Label("Parent"); 55 | 56 | GUILayout.BeginHorizontal(); 57 | 58 | GUILayout.Label(ourParent ? ourParent.ToString() : "None", GUILayout.Width(100)); 59 | 60 | if (MergeButton(">>>", usingOurs)) 61 | { 62 | UseOurs(); 63 | } 64 | 65 | var c = GUI.backgroundColor; 66 | GUI.backgroundColor = Color.white; 67 | var newParent = EditorGUILayout.ObjectField(transform.parent, typeof(Transform), true, GUILayout.Width(170)) as Transform; 68 | if (newParent != transform.parent) 69 | { 70 | transform.parent = newParent; 71 | UsedNew(); 72 | } 73 | GUI.backgroundColor = c; 74 | 75 | if (MergeButton("<<<", usingTheirs)) 76 | { 77 | UseTheirs(); 78 | } 79 | 80 | GUILayout.Label(theirParent ? theirParent.ToString() : "None", GUILayout.Width(100)); 81 | 82 | GUILayout.EndHorizontal(); 83 | GUILayout.EndVertical(); 84 | } 85 | 86 | private static bool MergeButton(string text, bool green) 87 | { 88 | if (green) 89 | { 90 | GUI.color = Color.green; 91 | } 92 | bool result = GUILayout.Button(text, GUILayout.ExpandWidth(false)); 93 | GUI.color = Color.white; 94 | return result; 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /Editor/Utilities/PageView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEditor; 5 | using UnityEngine; 6 | 7 | namespace GitMerge.Utilities 8 | { 9 | public class PageView 10 | { 11 | public int PageIndex { get; set; } = 0; 12 | public int NumElementsPerPage { get; set; } = 10; 13 | 14 | private Vector2 scrollPosition; 15 | 16 | /// 17 | /// Draw a scroll view only a limited number of elements displayed. 18 | /// Add tool to change the page to display previous/next range of elements. 19 | /// 20 | /// The total number of elements to draw. 21 | /// Called for each element to draw. The element index to draw is passed as argument. 22 | public void Draw(int numMaxElements, Action callbackElementDraw) 23 | { 24 | GUILayout.BeginVertical(GUILayout.ExpandWidth(true)); 25 | { 26 | DrawPageContent(callbackElementDraw, numMaxElements); 27 | DrawPageNavigation(numMaxElements); 28 | } 29 | GUILayout.EndVertical(); 30 | } 31 | 32 | private void DrawPageContent(Action callbackElementDraw, int numMaxElements) 33 | { 34 | scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true); 35 | { 36 | GUILayout.BeginVertical(GUILayout.ExpandWidth(true)); 37 | 38 | int lastElementIndex = Mathf.Min((PageIndex + 1) * NumElementsPerPage, numMaxElements); 39 | for (int index = PageIndex * NumElementsPerPage; index < lastElementIndex; ++index) 40 | { 41 | callbackElementDraw(index); 42 | } 43 | 44 | GUILayout.EndVertical(); 45 | } 46 | GUILayout.EndScrollView(); 47 | } 48 | 49 | private void DrawPageNavigation(int numMaxElements) 50 | { 51 | int numPages = CalculateNumberOfPages(numMaxElements); 52 | 53 | if (numPages == 0) 54 | { 55 | return; 56 | } 57 | 58 | GUILayout.BeginHorizontal(); 59 | { 60 | EditorGUILayout.PrefixLabel("Count Per Page"); 61 | int newNumElementsPerPage = EditorGUILayout.DelayedIntField(NumElementsPerPage, GUILayout.Width(100)); 62 | if (newNumElementsPerPage != NumElementsPerPage) 63 | { 64 | NumElementsPerPage = Mathf.Max(newNumElementsPerPage, 1); 65 | numPages = CalculateNumberOfPages(numMaxElements); 66 | } 67 | 68 | GUILayout.FlexibleSpace(); 69 | 70 | EditorGUI.BeginDisabledGroup(PageIndex == 0); 71 | { 72 | if (GUILayout.Button("<")) 73 | { 74 | --PageIndex; 75 | } 76 | } 77 | EditorGUI.EndDisabledGroup(); 78 | 79 | int newPageIndex = EditorGUILayout.DelayedIntField(PageIndex + 1, GUILayout.Width(30)) - 1; 80 | PageIndex = Mathf.Clamp(newPageIndex, 0, numPages - 1); 81 | 82 | GUILayout.Label("/" + numPages); 83 | 84 | EditorGUI.BeginDisabledGroup(PageIndex == numPages - 1); 85 | { 86 | if (GUILayout.Button(">")) 87 | { 88 | ++PageIndex; 89 | } 90 | } 91 | EditorGUI.EndDisabledGroup(); 92 | } 93 | GUILayout.EndHorizontal(); 94 | } 95 | 96 | private int CalculateNumberOfPages(int numMaxElements) 97 | { 98 | int numPages = numMaxElements / NumElementsPerPage; 99 | if (numMaxElements % NumElementsPerPage != 0) 100 | { 101 | ++numPages; 102 | } 103 | 104 | return numPages; 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /Editor/Resources/GitMergeStyles.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 67422f4e577bb17409644dcfe6bf75c8, type: 3} 13 | m_Name: GitMergeStyles 14 | m_EditorClassIdentifier: 15 | mergeActions: 16 | m_Name: 17 | m_Normal: 18 | m_Background: {fileID: 2800000, guid: be9ca0515e031fb4c86b12a6b9ba2410, type: 3} 19 | m_ScaledBackgrounds: [] 20 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 21 | m_Hover: 22 | m_Background: {fileID: 0} 23 | m_ScaledBackgrounds: [] 24 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 25 | m_Active: 26 | m_Background: {fileID: 0} 27 | m_ScaledBackgrounds: [] 28 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 29 | m_Focused: 30 | m_Background: {fileID: 0} 31 | m_ScaledBackgrounds: [] 32 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 33 | m_OnNormal: 34 | m_Background: {fileID: 0} 35 | m_ScaledBackgrounds: [] 36 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 37 | m_OnHover: 38 | m_Background: {fileID: 0} 39 | m_ScaledBackgrounds: [] 40 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 41 | m_OnActive: 42 | m_Background: {fileID: 0} 43 | m_ScaledBackgrounds: [] 44 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 45 | m_OnFocused: 46 | m_Background: {fileID: 0} 47 | m_ScaledBackgrounds: [] 48 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 49 | m_Border: 50 | m_Left: 4 51 | m_Right: 4 52 | m_Top: 4 53 | m_Bottom: 4 54 | m_Margin: 55 | m_Left: 2 56 | m_Right: 2 57 | m_Top: 2 58 | m_Bottom: 2 59 | m_Padding: 60 | m_Left: 4 61 | m_Right: 4 62 | m_Top: 4 63 | m_Bottom: 4 64 | m_Overflow: 65 | m_Left: 0 66 | m_Right: 0 67 | m_Top: 0 68 | m_Bottom: 0 69 | m_Font: {fileID: 0} 70 | m_FontSize: 0 71 | m_FontStyle: 0 72 | m_Alignment: 0 73 | m_WordWrap: 0 74 | m_RichText: 1 75 | m_TextClipping: 0 76 | m_ImagePosition: 0 77 | m_ContentOffset: {x: 0, y: 0} 78 | m_FixedWidth: 0 79 | m_FixedHeight: 0 80 | m_StretchWidth: 1 81 | m_StretchHeight: 0 82 | mergeAction: 83 | m_Name: 84 | m_Normal: 85 | m_Background: {fileID: 2800000, guid: be9ca0515e031fb4c86b12a6b9ba2410, type: 3} 86 | m_ScaledBackgrounds: [] 87 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 88 | m_Hover: 89 | m_Background: {fileID: 0} 90 | m_ScaledBackgrounds: [] 91 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 92 | m_Active: 93 | m_Background: {fileID: 0} 94 | m_ScaledBackgrounds: [] 95 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 96 | m_Focused: 97 | m_Background: {fileID: 0} 98 | m_ScaledBackgrounds: [] 99 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 100 | m_OnNormal: 101 | m_Background: {fileID: 0} 102 | m_ScaledBackgrounds: [] 103 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 104 | m_OnHover: 105 | m_Background: {fileID: 0} 106 | m_ScaledBackgrounds: [] 107 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 108 | m_OnActive: 109 | m_Background: {fileID: 0} 110 | m_ScaledBackgrounds: [] 111 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 112 | m_OnFocused: 113 | m_Background: {fileID: 0} 114 | m_ScaledBackgrounds: [] 115 | m_TextColor: {r: 0, g: 0, b: 0, a: 1} 116 | m_Border: 117 | m_Left: 4 118 | m_Right: 4 119 | m_Top: 4 120 | m_Bottom: 4 121 | m_Margin: 122 | m_Left: 2 123 | m_Right: 2 124 | m_Top: 2 125 | m_Bottom: 2 126 | m_Padding: 127 | m_Left: 2 128 | m_Right: 2 129 | m_Top: 2 130 | m_Bottom: 4 131 | m_Overflow: 132 | m_Left: 0 133 | m_Right: 0 134 | m_Top: 0 135 | m_Bottom: 0 136 | m_Font: {fileID: 0} 137 | m_FontSize: 0 138 | m_FontStyle: 0 139 | m_Alignment: 0 140 | m_WordWrap: 0 141 | m_RichText: 1 142 | m_TextClipping: 0 143 | m_ImagePosition: 0 144 | m_ContentOffset: {x: 0, y: 0} 145 | m_FixedWidth: 0 146 | m_FixedHeight: 0 147 | m_StretchWidth: 1 148 | m_StretchHeight: 0 149 | -------------------------------------------------------------------------------- /Editor/Utilities/GameObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using System.Text; 5 | using UnityEngine; 6 | using UnityEditor; 7 | 8 | public static class GameObjectExtensions 9 | { 10 | /// 11 | /// Adds the copy of a Component to a GameObject. 12 | /// 13 | /// The GameObject that will get the new Component 14 | /// The original component to copy 15 | /// The reference to the newly added Component copy 16 | public static Component AddComponent(this GameObject go, Component original) 17 | { 18 | var newComponent = go.AddComponent(original.GetType()); 19 | 20 | var originalProperty = new SerializedObject(original).GetIterator(); 21 | var newSerializedObject = new SerializedObject(newComponent); 22 | var newProperty = newSerializedObject.GetIterator(); 23 | 24 | if (originalProperty.Next(true)) 25 | { 26 | newProperty.Next(true); 27 | 28 | while (originalProperty.NextVisible(true)) 29 | { 30 | newProperty.NextVisible(true); 31 | newProperty.SetValue(originalProperty.GetValue()); 32 | } 33 | } 34 | 35 | newSerializedObject.ApplyModifiedProperties(); 36 | 37 | return newComponent; 38 | } 39 | 40 | /// 41 | /// Activates/deactivates the GameObjct, and hides it when it is disabled. 42 | /// This is used for "their" objects to hide them while merging. 43 | /// 44 | /// The object do enable/disable 45 | /// Enable or disable the object? 46 | public static void SetActiveForMerging(this GameObject go, bool active) 47 | { 48 | go.SetActive(active); 49 | go.hideFlags = active ? HideFlags.None : HideFlags.HideAndDontSave; 50 | } 51 | 52 | /// 53 | /// Ping the GameObject in the hierarchy, select it, and center it in the scene view. 54 | /// 55 | /// The GameObject of interest 56 | public static void Highlight(this GameObject go) 57 | { 58 | // Focussing on the same object twice, zooms in to the coordinate instead of the bounding box. 59 | if (Selection.activeObject == go) { 60 | return; 61 | } 62 | 63 | Selection.activeGameObject = go; 64 | EditorGUIUtility.PingObject(go); 65 | 66 | var view = SceneView.lastActiveSceneView; 67 | if (view) 68 | { 69 | view.FrameSelected(); 70 | } 71 | } 72 | 73 | /// 74 | /// Gets the path of this GameObject in the hierarchy. 75 | /// 76 | public static string GetPath(this GameObject gameObject) 77 | { 78 | var t = gameObject.transform; 79 | var sb = new StringBuilder(RemovePostfix(t.name)); 80 | while (t.parent != null) 81 | { 82 | t = t.parent; 83 | sb.Insert(0, RemovePostfix(t.name) + "/"); 84 | } 85 | return sb.ToString(); 86 | } 87 | 88 | /// 89 | /// Returns a child of this GameObject that has the same relative path to this GamObject as 90 | /// the given GameObject has to it's root GameObject. 91 | /// 92 | public static GameObject GetChildWithEqualPath(this GameObject gameObject, GameObject otherGameObject) 93 | { 94 | string fullHierarchyPath = otherGameObject.GetPath(); 95 | string relativeHierarchyPath = fullHierarchyPath.Substring(fullHierarchyPath.IndexOf("/", 1) + 1); 96 | Transform result = gameObject.transform.Find(relativeHierarchyPath); 97 | return result != null ? result.gameObject : gameObject; // fallback to root if a GameObject with equal path doesn't exist 98 | } 99 | 100 | private static string RemovePostfix(string name) 101 | { 102 | if (name.EndsWith(MergeManagerBase.THEIR_FILE_POSTFIX)) 103 | { 104 | return name.Substring(0, name.Length - MergeManagerBase.THEIR_FILE_POSTFIX.Length); 105 | } 106 | 107 | return name; 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /Editor/Utilities/MergeFilter.cs: -------------------------------------------------------------------------------- 1 | using GitMerge; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Text.RegularExpressions; 6 | using UnityEngine; 7 | 8 | namespace GitMerge 9 | { 10 | public class MergeFilter 11 | { 12 | public event Action OnChanged; 13 | 14 | public enum FilterMode 15 | { 16 | Inclusion, 17 | Exclusion 18 | } 19 | 20 | [System.Flags] 21 | public enum FilterState 22 | { 23 | Conflict = 0x01, 24 | Done = 0x02 25 | } 26 | 27 | private bool _useFilter = false; 28 | public bool useFilter 29 | { 30 | get => _useFilter; 31 | set 32 | { 33 | if (_useFilter != value) 34 | { 35 | _useFilter = value; 36 | OnChanged?.Invoke(); 37 | } 38 | } 39 | } 40 | 41 | private bool _isRegex = false; 42 | public bool isRegex 43 | { 44 | get => _isRegex; 45 | set 46 | { 47 | if (_isRegex != value) 48 | { 49 | _isRegex = value; 50 | OnChanged?.Invoke(); 51 | } 52 | } 53 | } 54 | 55 | private bool _isCaseSensitive = false; 56 | public bool isCaseSensitive 57 | { 58 | get => _isCaseSensitive; 59 | set 60 | { 61 | if (_isCaseSensitive != value) 62 | { 63 | _isCaseSensitive = value; 64 | OnChanged?.Invoke(); 65 | } 66 | } 67 | } 68 | 69 | private string _expression = string.Empty; 70 | public string expression 71 | { 72 | get => _expression; 73 | set 74 | { 75 | if (_expression != value) 76 | { 77 | _expression = value; 78 | regex = new Regex(_expression); 79 | OnChanged?.Invoke(); 80 | } 81 | } 82 | } 83 | 84 | private FilterMode _filterMode = FilterMode.Inclusion; 85 | public FilterMode filterMode 86 | { 87 | get => _filterMode; 88 | set 89 | { 90 | if (_filterMode != value) 91 | { 92 | _filterMode = value; 93 | OnChanged?.Invoke(); 94 | } 95 | } 96 | } 97 | 98 | private FilterState _filterState = (FilterState)(-1); 99 | public FilterState filterState 100 | { 101 | get => _filterState; 102 | set 103 | { 104 | if (_filterState != value) 105 | { 106 | _filterState = value; 107 | OnChanged?.Invoke(); 108 | } 109 | } 110 | } 111 | 112 | private Regex regex = new Regex(string.Empty); 113 | 114 | public bool IsPassingFilter(GameObjectMergeActions action) 115 | { 116 | if (!useFilter) 117 | { 118 | return true; 119 | } 120 | 121 | bool isPassingFilter = false; 122 | 123 | string name = action.name; 124 | 125 | if (isRegex) 126 | { 127 | isPassingFilter = regex.IsMatch(name); 128 | } 129 | else 130 | { 131 | if (!isCaseSensitive) 132 | { 133 | name = name.ToLowerInvariant(); 134 | isPassingFilter = name.Contains(_expression.ToLowerInvariant()); 135 | } 136 | else 137 | { 138 | isPassingFilter = name.Contains(_expression); 139 | } 140 | } 141 | 142 | if (filterMode == FilterMode.Exclusion) 143 | { 144 | isPassingFilter = !isPassingFilter; 145 | } 146 | 147 | bool isPassingFilterState = false; 148 | if ((_filterState & FilterState.Conflict) != 0) 149 | { 150 | isPassingFilterState |= !action.merged; 151 | } 152 | 153 | if ((_filterState & FilterState.Done) != 0) 154 | { 155 | isPassingFilterState |= action.merged; 156 | } 157 | isPassingFilter &= isPassingFilterState; 158 | 159 | return isPassingFilter; 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /Editor/MergeManagerBase.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | using System.IO; 6 | using System.Collections.Generic; 7 | 8 | public abstract class MergeManagerBase 9 | { 10 | internal const string OUR_FILE_POSTFIX = "--OURS"; 11 | internal const string THEIR_FILE_POSTFIX = "--THEIRS"; 12 | 13 | protected VCS vcs { private set; get; } 14 | protected GitMergeWindow window { private set; get; } 15 | 16 | internal List allMergeActions; 17 | 18 | protected static string fileName; 19 | protected static string theirFilename; 20 | 21 | public static bool isMergingScene { protected set; get; } 22 | public static bool isMergingPrefab { get { return !isMergingScene; } } 23 | 24 | 25 | public MergeManagerBase(GitMergeWindow window, VCS vcs) 26 | { 27 | this.window = window; 28 | this.vcs = vcs; 29 | allMergeActions = new List(); 30 | } 31 | 32 | /// 33 | /// Creates "their" version of the file at the given path, 34 | /// named filename--THEIRS.unity. 35 | /// 36 | /// The path of the file, relative to the project folder. 37 | protected void CheckoutTheirVersionOf(string path) 38 | { 39 | fileName = path; 40 | 41 | string basepath = Path.GetDirectoryName(path); 42 | string sceneName = Path.GetFileNameWithoutExtension(path); 43 | string extension = Path.GetExtension(path); 44 | 45 | string ourFilename = Path.Combine(basepath, sceneName + OUR_FILE_POSTFIX + extension); 46 | theirFilename = Path.Combine(basepath, sceneName + THEIR_FILE_POSTFIX + extension); 47 | 48 | File.Copy(path, ourFilename); 49 | try 50 | { 51 | vcs.CheckoutTheirs(path); 52 | } 53 | catch (VCSException e) 54 | { 55 | File.Delete(ourFilename); 56 | throw e; 57 | } 58 | File.Move(path, theirFilename); 59 | File.Move(ourFilename, path); 60 | } 61 | 62 | /// 63 | /// Finds all specific merge conflicts between two sets of GameObjects, 64 | /// representing "our" scene and "their" scene. 65 | /// 66 | /// The GameObjects of "our" version of the scene. 67 | /// The GameObjects of "their" version of the scene. 68 | protected void BuildAllMergeActions(List ourObjects, List theirObjects) 69 | { 70 | allMergeActions = new List(); 71 | 72 | // Map "their" GameObjects to their respective ids 73 | var theirObjectsDict = new Dictionary(); 74 | foreach (var theirs in theirObjects) 75 | { 76 | theirObjectsDict.Add(ObjectID.GetFor(theirs), theirs); 77 | } 78 | 79 | foreach (var ours in ourObjects) 80 | { 81 | // Try to find "their" equivalent to "our" GameObjects 82 | var id = ObjectID.GetFor(ours); 83 | GameObject theirs; 84 | theirObjectsDict.TryGetValue(id, out theirs); 85 | 86 | // If theirs is null, mergeActions.hasActions will be false 87 | var mergeActions = new GameObjectMergeActions(ours, theirs); 88 | if (mergeActions.hasActions) 89 | { 90 | allMergeActions.Add(mergeActions); 91 | } 92 | // Remove "their" GameObject from the dict to only keep those new to us 93 | theirObjectsDict.Remove(id); 94 | } 95 | 96 | // Every GameObject left in the dict is a... 97 | foreach (var theirs in theirObjectsDict.Values) 98 | { 99 | // ...new GameObject from them 100 | var mergeActions = new GameObjectMergeActions(null, theirs); 101 | if (mergeActions.hasActions) 102 | { 103 | allMergeActions.Add(mergeActions); 104 | } 105 | } 106 | } 107 | 108 | public abstract void CompleteMerge(); 109 | 110 | public virtual void AbortMerge(bool showNotification = true) 111 | { 112 | MergeAction.inMergePhase = false; 113 | 114 | foreach (var actions in allMergeActions) 115 | { 116 | actions.UseOurs(); 117 | } 118 | ObjectDictionaries.DestroyTheirObjects(); 119 | ObjectDictionaries.Clear(); 120 | allMergeActions = null; 121 | 122 | if (showNotification) 123 | { 124 | window.ShowNotification(new GUIContent("Merge aborted.")); 125 | } 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /Editor/MergeActions/MergeAction.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | using UnityEditor; 6 | 7 | /// 8 | /// Each MergeAction represents a single, specific merge conflict. 9 | /// This can be a GameObject added or deleted in one of the versions, 10 | /// a Component added or deleted on a GameObject, 11 | /// or a single property changed on a Component. 12 | /// 13 | public abstract class MergeAction 14 | { 15 | // Don't highlight objects if not in merge phase. 16 | // Prevents highlighting while automerging. 17 | public static bool inMergePhase; 18 | 19 | // A MergeAction is considered "merged" when, at some point, 20 | // "our", "their" or a new version has been applied. 21 | public bool merged { protected set; get; } 22 | 23 | public GameObject ours { protected set; get; } 24 | public GameObject theirs { protected set; get; } 25 | 26 | // Flags that indicate how this MergeAction has been resolved. 27 | protected bool usingOurs; 28 | protected bool usingTheirs; 29 | protected bool usingNew; 30 | 31 | protected bool wasResolvedAutomatically; 32 | 33 | 34 | public MergeAction(GameObject ours, GameObject theirs) 35 | { 36 | this.ours = ours; 37 | this.theirs = theirs; 38 | } 39 | 40 | /// 41 | /// Apply "our" change in the conflict, dismissing "their"s. 42 | /// 43 | public void UseOurs() 44 | { 45 | try 46 | { 47 | ApplyOurs(); 48 | } 49 | catch 50 | { 51 | return; 52 | } 53 | merged = true; 54 | usingOurs = true; 55 | usingTheirs = false; 56 | usingNew = false; 57 | 58 | wasResolvedAutomatically = !inMergePhase; 59 | 60 | if (GitMergeWindow.autofocus) 61 | { 62 | HighlightObject(); 63 | } 64 | 65 | RefreshPrefabInstance(); 66 | } 67 | 68 | /// 69 | /// Apply "their" change in the conflict, dismissing "our"s. 70 | /// 71 | public void UseTheirs() 72 | { 73 | try 74 | { 75 | ApplyTheirs(); 76 | } 77 | catch 78 | { 79 | return; 80 | } 81 | merged = true; 82 | usingOurs = false; 83 | usingTheirs = true; 84 | usingNew = false; 85 | 86 | wasResolvedAutomatically = !inMergePhase; 87 | 88 | if (GitMergeWindow.autofocus) 89 | { 90 | HighlightObject(); 91 | } 92 | 93 | RefreshPrefabInstance(); 94 | } 95 | 96 | /// 97 | /// Mark this to use a new value instead of either of the conflicting ones. 98 | /// 99 | public void UsedNew() 100 | { 101 | merged = true; 102 | usingOurs = false; 103 | usingTheirs = false; 104 | usingNew = true; 105 | 106 | wasResolvedAutomatically = !inMergePhase; 107 | 108 | RefreshPrefabInstance(); 109 | } 110 | 111 | /// 112 | /// Refreshes the prefab instance, if there is any. 113 | /// We change the prefab directly, so we have to do this to see the changes in the scene view. 114 | /// 115 | private static void RefreshPrefabInstance() 116 | { 117 | if (MergeManagerBase.isMergingPrefab) 118 | { 119 | PrefabUtility.RevertObjectOverride(MergeManagerPrefab.ourPrefabInstance, InteractionMode.AutomatedAction); 120 | } 121 | } 122 | 123 | // The implementations of these methods conatain the actual merging steps 124 | protected abstract void ApplyOurs(); 125 | protected abstract void ApplyTheirs(); 126 | 127 | /// 128 | /// Displays the MergeAction. 129 | /// 130 | /// True when the represented conflict has now been merged. 131 | public bool OnGUIMerge() 132 | { 133 | var wasMerged = merged; 134 | if (merged) 135 | { 136 | GUI.backgroundColor = wasResolvedAutomatically ? new Color(.9f, .9f, .3f, 1) : new Color(.2f, .8f, .2f, 1); 137 | } 138 | else 139 | { 140 | GUI.backgroundColor = new Color(1f, .25f, .25f, 1); 141 | } 142 | GUILayout.BeginHorizontal(Resources.styles.mergeAction); 143 | GUI.backgroundColor = Color.white; 144 | OnGUI(); 145 | GUI.color = Color.white; 146 | GUILayout.EndHorizontal(); 147 | return merged && !wasMerged; 148 | } 149 | 150 | // The actual UI of the MergeAction depends on the actual type 151 | public abstract void OnGUI(); 152 | 153 | private void HighlightObject() 154 | { 155 | // Highlight the instance of the prefab, not the prefab itself 156 | // Otherwise, "ours". 157 | var objectToHighlight = MergeManagerBase.isMergingPrefab ? MergeManagerPrefab.ourPrefabInstance.GetChildWithEqualPath(ours) : ours; 158 | if (objectToHighlight && inMergePhase && objectToHighlight.hideFlags == HideFlags.None) 159 | { 160 | objectToHighlight.Highlight(); 161 | } 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /Editor/MergeManagerScene.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | using UnityEditor; 6 | using UnityEngine.SceneManagement; 7 | using UnityEditor.SceneManagement; 8 | using System.Collections.Generic; 9 | 10 | public class MergeManagerScene : MergeManagerBase 11 | { 12 | private const int NUMBER_OF_INITIALIZATION_STEPS = 3; 13 | 14 | private Scene theirScene; 15 | 16 | public MergeManagerScene(GitMergeWindow window, VCS vcs) 17 | : base(window, vcs) 18 | { 19 | 20 | } 21 | 22 | public bool TryInitializeMerge(string path = null) 23 | { 24 | var activeScene = EditorSceneManager.GetActiveScene(); 25 | 26 | if (activeScene.isDirty) 27 | { 28 | window.ShowNotification(new GUIContent("Please make sure there are no unsaved changes before attempting to merge.")); 29 | return false; 30 | } 31 | 32 | DisplayProgressBar(0, "Checking out scene..."); 33 | isMergingScene = true; 34 | var scenePath = path ?? activeScene.path; 35 | 36 | // Overwrite the current scene to prevent the reload/ignore dialog that pops up after the upcoming changes to the file. 37 | // Pressing "reload" on it would invalidate the GameObject references we're about to collect. 38 | EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); 39 | Lightmapping.ForceStop(); 40 | 41 | vcs.CheckoutOurs(scenePath); 42 | CheckoutTheirVersionOf(scenePath); 43 | AssetDatabase.Refresh(); 44 | 45 | DisplayProgressBar(1, "Opening scene..."); 46 | 47 | activeScene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); 48 | 49 | MergeAction.inMergePhase = false; 50 | ObjectDictionaries.Clear(); 51 | 52 | List ourObjects; 53 | try 54 | { 55 | DisplayProgressBar(2, "Collecting differences..."); 56 | // Find all of "our" objects 57 | ourObjects = GetAllSceneObjects(); 58 | ObjectDictionaries.AddToOurObjects(ourObjects); 59 | 60 | // Add "their" objects 61 | theirScene = EditorSceneManager.OpenScene(theirFilename, OpenSceneMode.Additive); 62 | 63 | var addedObjects = GetAllNewSceneObjects(ourObjects); 64 | ObjectDictionaries.AddToTheirObjects(addedObjects); 65 | BuildAllMergeActions(ourObjects, addedObjects); 66 | 67 | MoveGameObjectsToScene(theirScene.GetRootGameObjects(), activeScene); 68 | } 69 | finally 70 | { 71 | EditorSceneManager.UnloadSceneAsync(theirScene); 72 | AssetDatabase.DeleteAsset(theirFilename); 73 | 74 | EditorUtility.ClearProgressBar(); 75 | } 76 | 77 | if (allMergeActions.Count == 0) 78 | { 79 | window.ShowNotification(new GUIContent("No conflict found for this scene.")); 80 | return false; 81 | } 82 | 83 | MergeAction.inMergePhase = true; 84 | return true; 85 | } 86 | 87 | private static void DisplayProgressBar(int step, string text) 88 | { 89 | var progress = step / (float)NUMBER_OF_INITIALIZATION_STEPS; 90 | EditorUtility.DisplayProgressBar("GitMerge for Unity", text, progress); 91 | } 92 | 93 | private static void MoveGameObjectsToScene(IEnumerable addedObjects, Scene scene) 94 | { 95 | foreach (var obj in addedObjects) 96 | { 97 | EditorSceneManager.MoveGameObjectToScene(obj, scene); 98 | } 99 | } 100 | 101 | private static List GetAllSceneObjects() 102 | { 103 | var objects = (GameObject[])Object.FindObjectsOfType(typeof(GameObject)); 104 | return new List(objects); 105 | } 106 | 107 | /// 108 | /// Finds all GameObjects in the scene, minus the ones passed. 109 | /// 110 | private static List GetAllNewSceneObjects(List oldObjects) 111 | { 112 | var all = GetAllSceneObjects(); 113 | 114 | foreach (var obj in oldObjects) 115 | { 116 | all.Remove(obj); 117 | } 118 | 119 | return all; 120 | } 121 | 122 | /// 123 | /// Completes the merge process after solving all conflicts. 124 | /// Cleans up the scene by deleting "their" GameObjects, clears merge related data structures, 125 | /// executes git add scene_name. 126 | /// 127 | public override void CompleteMerge() 128 | { 129 | MergeAction.inMergePhase = false; 130 | 131 | ObjectDictionaries.DestroyTheirObjects(); 132 | ObjectDictionaries.Clear(); 133 | EditorSceneManager.SaveScene(EditorSceneManager.GetActiveScene()); 134 | 135 | allMergeActions = null; 136 | 137 | vcs.MarkAsMerged(fileName); 138 | 139 | // Directly committing here might not be that smart, since there might be more conflicts 140 | 141 | window.ShowNotification(new GUIContent("Scene successfully merged.")); 142 | } 143 | 144 | /// 145 | /// Aborts merge by using "our" version in all conflicts. 146 | /// Cleans up merge related data. 147 | /// 148 | public override void AbortMerge(bool showNotification = true) 149 | { 150 | base.AbortMerge(showNotification); 151 | 152 | EditorSceneManager.CloseScene(theirScene, true); 153 | 154 | EditorSceneManager.SaveScene(EditorSceneManager.GetActiveScene()); 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /Editor/MergeManagerPrefab.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | using UnityEditor; 6 | using System.Collections.Generic; 7 | using UnityEditor.SceneManagement; 8 | 9 | public class MergeManagerPrefab : MergeManagerBase 10 | { 11 | public static GameObject ourPrefab { private set; get; } 12 | private static GameObject theirPrefab; 13 | public static GameObject ourPrefabInstance { private set; get; } 14 | private static string previouslyOpenedScenePath; 15 | 16 | 17 | public MergeManagerPrefab(GitMergeWindow window, VCS vcs) 18 | : base(window, vcs) 19 | { 20 | 21 | } 22 | 23 | public bool TryInitializeMerge(string prefabPath) 24 | { 25 | if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) 26 | { 27 | return false; 28 | } 29 | 30 | isMergingScene = false; 31 | MergeAction.inMergePhase = false; 32 | 33 | ObjectDictionaries.Clear(); 34 | 35 | vcs.CheckoutOurs(prefabPath); 36 | CheckoutTheirVersionOf(prefabPath); 37 | AssetDatabase.Refresh(); 38 | 39 | ourPrefab = AssetDatabase.LoadAssetAtPath(prefabPath); 40 | 41 | if (ourPrefab == null) 42 | { 43 | DeleteTheirPrefabAndLoadPreviousScene(); 44 | return false; 45 | } 46 | 47 | // Open a new Scene that will only display the prefab. 48 | previouslyOpenedScenePath = EditorSceneManager.GetActiveScene().path; 49 | EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); 50 | Lightmapping.ForceStop(); 51 | 52 | // Instantiate our object in order to view it while merging. 53 | ourPrefabInstance = PrefabUtility.InstantiatePrefab(ourPrefab) as GameObject; 54 | 55 | // UI Elements need a Canvas to be displayed correctly: 56 | if (ourPrefabInstance.GetComponentInChildren() != null) { 57 | GameObject defaultCanvas = new GameObject("Canvas"); 58 | defaultCanvas.AddComponent().renderMode = RenderMode.ScreenSpaceOverlay; 59 | ourPrefabInstance.transform.SetParent(defaultCanvas.transform, false); 60 | } 61 | 62 | var ourObjects = GetAllObjects(ourPrefab); 63 | 64 | theirPrefab = AssetDatabase.LoadAssetAtPath(theirFilename, typeof(GameObject)) as GameObject; 65 | var theirObjects = GetAllObjects(theirPrefab); 66 | 67 | BuildAllMergeActions(ourObjects, theirObjects); 68 | 69 | AssetDatabase.DeleteAsset(theirFilename); 70 | 71 | if (allMergeActions.Count == 0) 72 | { 73 | DeleteTheirPrefabAndLoadPreviousScene(); 74 | window.ShowNotification(new GUIContent("No conflict found for this prefab.")); 75 | return false; 76 | } 77 | MergeAction.inMergePhase = true; 78 | ourPrefabInstance.Highlight(); 79 | return true; 80 | } 81 | 82 | private static void DeleteTheirPrefabAndLoadPreviousScene() 83 | { 84 | AssetDatabase.DeleteAsset(theirFilename); 85 | OpenPreviousScene(); 86 | } 87 | 88 | /// 89 | /// Recursively find all GameObjects that are part of the prefab 90 | /// 91 | /// The prefab to analyze 92 | /// The list with all the objects already found. Pass null in the beginning. 93 | /// The list with all the objects 94 | private static List GetAllObjects(GameObject prefab, List list = null) 95 | { 96 | if (list == null) 97 | { 98 | list = new List(); 99 | } 100 | 101 | list.Add(prefab); 102 | foreach (Transform t in prefab.transform) 103 | { 104 | GetAllObjects(t.gameObject, list); 105 | } 106 | return list; 107 | } 108 | 109 | /// 110 | /// Completes the merge process after solving all conflicts. 111 | /// Cleans up the scene by deleting "their" GameObjects, clears merge related data structures, 112 | /// executes git add scene_name. 113 | /// 114 | public override void CompleteMerge() 115 | { 116 | MergeAction.inMergePhase = false; 117 | 118 | // ObjectDictionaries.Clear(); 119 | 120 | allMergeActions = null; 121 | 122 | // TODO: Could we explicitly just save the prefab? 123 | AssetDatabase.SaveAssets(); 124 | 125 | vcs.MarkAsMerged(fileName); 126 | 127 | // Directly committing here might not be that smart, since there might be more conflicts. 128 | 129 | ourPrefab = null; 130 | 131 | DeleteTheirPrefabAndLoadPreviousScene(); 132 | window.ShowNotification(new GUIContent("Prefab successfully merged.")); 133 | } 134 | 135 | /// 136 | /// Aborts merge by using "our" version in all conflicts. 137 | /// Cleans up merge related data. 138 | /// 139 | public override void AbortMerge(bool showNotification = true) 140 | { 141 | base.AbortMerge(showNotification); 142 | 143 | DeleteTheirPrefabAndLoadPreviousScene(); 144 | ourPrefab = null; 145 | } 146 | 147 | /// 148 | /// Opens the previously opened scene, if there was any. 149 | /// 150 | private static void OpenPreviousScene() 151 | { 152 | if (!string.IsNullOrEmpty(previouslyOpenedScenePath)) 153 | { 154 | EditorSceneManager.OpenScene(previouslyOpenedScenePath); 155 | previouslyOpenedScenePath = null; 156 | } 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /Editor/Utilities/SerializedPropertyExtensions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace GitMerge 5 | { 6 | public static class SerializedPropertyExtensions 7 | { 8 | public static object GetValue(this SerializedProperty p) 9 | { 10 | switch (p.propertyType) 11 | { 12 | case SerializedPropertyType.AnimationCurve: 13 | return p.animationCurveValue; 14 | case SerializedPropertyType.ArraySize: 15 | return p.intValue; 16 | case SerializedPropertyType.Boolean: 17 | return p.boolValue; 18 | case SerializedPropertyType.Bounds: 19 | return p.boundsValue; 20 | case SerializedPropertyType.Character: 21 | return p.stringValue; //TODO: might be bullshit 22 | case SerializedPropertyType.Color: 23 | return p.colorValue; 24 | case SerializedPropertyType.Enum: 25 | return p.enumValueIndex; 26 | case SerializedPropertyType.Float: 27 | return p.floatValue; 28 | case SerializedPropertyType.Generic: //(arrays) 29 | if (p.isArray) 30 | { 31 | var arr = new object[p.arraySize]; 32 | for (int i = 0; i < arr.Length; ++i) 33 | { 34 | arr[i] = p.GetArrayElementAtIndex(i).GetValue(); 35 | } 36 | return arr; 37 | } 38 | else 39 | { 40 | return null; 41 | } 42 | case SerializedPropertyType.Gradient: 43 | return 0; //TODO: erm 44 | case SerializedPropertyType.Integer: 45 | return p.intValue; 46 | case SerializedPropertyType.LayerMask: 47 | return p.intValue; 48 | case SerializedPropertyType.ObjectReference: 49 | return p.objectReferenceValue; 50 | case SerializedPropertyType.Quaternion: 51 | return p.quaternionValue; 52 | case SerializedPropertyType.Rect: 53 | return p.rectValue; 54 | case SerializedPropertyType.String: 55 | return p.stringValue; 56 | case SerializedPropertyType.Vector2: 57 | return p.vector2Value; 58 | case SerializedPropertyType.Vector3: 59 | return p.vector3Value; 60 | case SerializedPropertyType.Vector4: 61 | return p.vector4Value; 62 | default: 63 | return 0; 64 | } 65 | } 66 | 67 | public static void SetValue(this SerializedProperty p, object value) 68 | { 69 | switch (p.propertyType) 70 | { 71 | case SerializedPropertyType.AnimationCurve: 72 | p.animationCurveValue = value as AnimationCurve; 73 | break; 74 | case SerializedPropertyType.ArraySize: 75 | //TODO: erm 76 | p.intValue = (int)value; 77 | break; 78 | case SerializedPropertyType.Boolean: 79 | p.boolValue = (bool)value; 80 | break; 81 | case SerializedPropertyType.Bounds: 82 | p.boundsValue = (Bounds)value; 83 | break; 84 | case SerializedPropertyType.Character: 85 | p.stringValue = (string)value; //TODO: might be bullshit 86 | break; 87 | case SerializedPropertyType.Color: 88 | p.colorValue = (Color)value; 89 | break; 90 | case SerializedPropertyType.Enum: 91 | p.enumValueIndex = (int)value; 92 | break; 93 | case SerializedPropertyType.Float: 94 | p.floatValue = (float)value; 95 | break; 96 | case SerializedPropertyType.Generic: //(arrays) 97 | if (p.isArray) 98 | { 99 | //var size = p.arraySize; 100 | //var resetPath = p.propertyPath; 101 | var values = (object[])value; 102 | /* 103 | for(int i = 0; i < p.arraySize; ++i) 104 | { 105 | Debug.Log(i + ": " + p.GetArrayElementAtIndex(i).GetValue()); 106 | } 107 | */ 108 | p.ClearArray(); 109 | for (int i = 0; i < values.Length; ++i) 110 | { 111 | p.InsertArrayElementAtIndex(i); 112 | //Debug.Log(i + ": " + pv.GetArrayElementAtIndex(i).GetValue()); 113 | p.GetArrayElementAtIndex(i).SetValue(values[i]); 114 | } 115 | 116 | //p.FindPropertyRelative(resetPath); 117 | } 118 | else 119 | { 120 | //p.stringValue = (string)value; 121 | } 122 | break; 123 | case SerializedPropertyType.Gradient: 124 | //TODO: erm 125 | break; 126 | case SerializedPropertyType.Integer: 127 | p.intValue = (int)value; 128 | break; 129 | case SerializedPropertyType.LayerMask: 130 | p.intValue = (int)value; 131 | break; 132 | case SerializedPropertyType.ObjectReference: 133 | p.objectReferenceValue = value as Object; //TODO: what about non-UnityEngine objects? 134 | break; 135 | case SerializedPropertyType.Quaternion: 136 | p.quaternionValue = (Quaternion)value; 137 | break; 138 | case SerializedPropertyType.Rect: 139 | p.rectValue = (Rect)value; 140 | break; 141 | case SerializedPropertyType.String: 142 | p.stringValue = (string)value; 143 | break; 144 | case SerializedPropertyType.Vector2: 145 | p.vector2Value = (Vector2)value; 146 | break; 147 | case SerializedPropertyType.Vector3: 148 | p.vector3Value = (Vector3)value; 149 | break; 150 | case SerializedPropertyType.Vector4: 151 | p.vector4Value = (Vector4)value; 152 | break; 153 | default: 154 | break; 155 | } 156 | } 157 | 158 | public static string GetPlainName(this SerializedProperty p) 159 | { 160 | var s = p.name; 161 | var i = s.IndexOf('_'); 162 | if (i >= 0) 163 | { 164 | s = s.Substring(i + 1); 165 | } 166 | return s; 167 | } 168 | 169 | public static bool IsRealArray(this SerializedProperty p) 170 | { 171 | return p.propertyType == SerializedPropertyType.Generic && p.isArray; 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /Editor/ObjectDictionaries.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | using System.Collections.Generic; 6 | 7 | /// 8 | /// Dictionaries that categorize the scene's objects into our objects, their objects, and temporary 9 | /// copies of their objects that have been instantiated while merging. 10 | /// 11 | public static class ObjectDictionaries 12 | { 13 | // This dict holds all of "our" objects. 14 | // Needed for Reference handling. 15 | private static Dictionary ourObjects = new Dictionary(); 16 | 17 | // This dict maps our instances of their objects. 18 | // Whenever we instantiate a copy of "their" new object, they're both added here. 19 | private static Dictionary ourInstances = new Dictionary(); 20 | 21 | // This dict holds all of "their" GameObjects. 22 | // Needed for scene cleaning after merge. 23 | // 24 | private static Dictionary theirObjects = new Dictionary(); 25 | 26 | // This dict holds all GameObjects that might or might not exist, 27 | // depending on the current merge state. The referenced objects are the versions that will definitely exist throughout the merge. 28 | // Also maps the MergeActions responsible for their existence to them. 29 | private static Dictionary schroedingersObjects = new Dictionary(); 30 | 31 | 32 | public static void AddToOurObjects(List objects) 33 | { 34 | foreach (var obj in objects) 35 | { 36 | SetAsOurObject(obj); 37 | } 38 | } 39 | 40 | public static void AddToTheirObjects(List objects) 41 | { 42 | foreach (var obj in objects) 43 | { 44 | SetAsTheirs(obj, false); 45 | } 46 | } 47 | 48 | 49 | public static void SetAsOurObject(GameObject go) 50 | { 51 | AddOurObject(go); 52 | foreach (var c in go.GetComponents()) 53 | { 54 | AddOurObject(c); 55 | } 56 | } 57 | 58 | public static void SetAsOurObject(Component c) 59 | { 60 | AddOurObject(c); 61 | } 62 | 63 | private static void AddOurObject(Object o) 64 | { 65 | if (o == null) 66 | return; 67 | 68 | ourObjects.Add(ObjectID.GetFor(o), o); 69 | } 70 | 71 | public static void RemoveOurObject(GameObject go) 72 | { 73 | foreach (var c in go.GetComponents()) 74 | { 75 | RemoveOurSingleObject(c); 76 | } 77 | RemoveOurSingleObject(go); 78 | } 79 | 80 | public static void RemoveOurObject(Component c) 81 | { 82 | RemoveOurSingleObject(c); 83 | } 84 | 85 | private static void RemoveOurSingleObject(Object o) 86 | { 87 | if (o == null) 88 | return; 89 | 90 | ourObjects.Remove(ObjectID.GetFor(o)); 91 | } 92 | 93 | public static Object GetOurObject(ObjectID id) 94 | { 95 | Object result = null; 96 | ourObjects.TryGetValue(id, out result); 97 | return result; 98 | } 99 | 100 | /// 101 | /// Returns: 102 | /// * the given object if it is "ours" 103 | /// * "our" counterpart of obj if it is "theirs" 104 | /// * null if the object is deleted for some reason 105 | /// The returned object can be an instance of "their" object temporarily added for the merge 106 | /// 107 | /// the original object 108 | /// the counterpart of the object in "our" version 109 | public static Object GetOurCounterpartFor(Object obj) 110 | { 111 | var result = obj; 112 | if (IsTheirs(obj)) 113 | { 114 | result = GetOurObject(ObjectID.GetFor(obj)); 115 | if (!result) 116 | { 117 | result = GetOurInstanceOfCopy(obj); 118 | } 119 | } 120 | return result; 121 | } 122 | 123 | public static void Clear() 124 | { 125 | ourObjects.Clear(); 126 | theirObjects.Clear(); 127 | ourInstances.Clear(); 128 | schroedingersObjects.Clear(); 129 | } 130 | 131 | /// 132 | /// Mark o as an instance of theirs 133 | /// 134 | public static void SetAsCopy(GameObject o, GameObject theirs) 135 | { 136 | ourInstances.Add(theirs, o); 137 | var instanceComponents = o.GetComponents(); 138 | var theirComponents = theirs.GetComponents(); 139 | for (int i = 0; i < instanceComponents.Length; ++i) 140 | { 141 | SetAsCopy(instanceComponents[i], theirComponents[i]); 142 | } 143 | } 144 | 145 | public static void SetAsCopy(Component c, Component theirs) 146 | { 147 | if (c == null) 148 | return; 149 | 150 | ourInstances.Add(theirs, c); 151 | } 152 | 153 | public static void RemoveCopyOf(GameObject theirs) 154 | { 155 | ourInstances.Remove(theirs); 156 | foreach (var c in theirs.GetComponents()) 157 | { 158 | if (c != null) 159 | { 160 | ourInstances.Remove(c); 161 | } 162 | } 163 | } 164 | 165 | public static void RemoveCopyOf(Component theirs) 166 | { 167 | ourInstances.Remove(theirs); 168 | } 169 | 170 | /// 171 | /// Returns: 172 | /// * the given object if it is "ours" 173 | /// * "our" instance of obj if it is "theirs" 174 | /// * null if there is no such instance 175 | /// 176 | /// the original object 177 | /// the instance of the original object 178 | public static Object GetOurInstanceOfCopy(Object obj) 179 | { 180 | var result = obj; 181 | if (IsTheirs(obj)) 182 | { 183 | ourInstances.TryGetValue(obj, out result); 184 | } 185 | return result; 186 | } 187 | 188 | private static bool IsTheirs(Object obj) 189 | { 190 | var go = obj as GameObject; 191 | if (go) 192 | { 193 | return theirObjects.ContainsKey(go); 194 | } 195 | var c = obj as Component; 196 | if (c) 197 | { 198 | return theirObjects.ContainsKey(c.gameObject); 199 | } 200 | return false; 201 | } 202 | 203 | public static void SetAsTheirs(GameObject go, bool active) 204 | { 205 | if (!theirObjects.ContainsKey(go)) 206 | { 207 | theirObjects.Add(go, go.activeSelf); 208 | } 209 | go.SetActiveForMerging(false); 210 | } 211 | 212 | /// 213 | /// Copy an object that has been disabled and hidden for merging into the scene, 214 | /// enable and unhide the copy. 215 | /// 216 | /// The original GameObject. 217 | /// The copy GameObject. 218 | public static GameObject InstantiateFromMerging(GameObject go) 219 | { 220 | var copy = GameObject.Instantiate(go) as GameObject; 221 | 222 | // Destroy children. 223 | foreach (Transform t in copy.GetComponent()) 224 | { 225 | Object.DestroyImmediate(t.gameObject); 226 | } 227 | 228 | bool wasActive; 229 | if (!theirObjects.TryGetValue(go, out wasActive)) 230 | { 231 | wasActive = go.activeSelf; 232 | } 233 | 234 | // Apply some special properties of the GameObject. 235 | copy.SetActive(wasActive); 236 | copy.hideFlags = HideFlags.None; 237 | copy.name = go.name; 238 | copy.GetComponent().parent = GetOurCounterpartFor(go.GetComponent().parent) as Transform; 239 | 240 | return copy; 241 | } 242 | 243 | public static void DestroyTheirObjects() 244 | { 245 | foreach (var obj in theirObjects.Keys) 246 | { 247 | if (obj != null && obj.transform.parent == null) 248 | { 249 | Object.DestroyImmediate(obj); 250 | } 251 | } 252 | theirObjects.Clear(); 253 | } 254 | 255 | public static void AddToSchroedingersObjects(GameObject go, MergeActionExistence mergeAction) 256 | { 257 | schroedingersObjects.Add(go, mergeAction); 258 | } 259 | 260 | public static void EnsureExistence(GameObject go) 261 | { 262 | schroedingersObjects[go].EnsureExistence(); 263 | } 264 | } 265 | } -------------------------------------------------------------------------------- /Editor/MergeActions/MergeActionChangeValues.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | using UnityEditor; 6 | using System.Linq; 7 | 8 | /// 9 | /// The MergeAction allowing to merge the value of a single property of a Component. 10 | /// 11 | public class MergeActionChangeValues : MergeAction 12 | { 13 | protected SerializedProperty ourProperty; 14 | protected SerializedProperty theirProperty; 15 | protected object ourInitialValue; 16 | protected object theirInitialValue; 17 | protected readonly string ourString; 18 | protected readonly string theirString; 19 | protected readonly string fieldname; 20 | 21 | public MergeActionChangeValues(GameObject ours, SerializedProperty ourProperty, SerializedProperty theirProperty) 22 | : base(ours, null) 23 | { 24 | this.ourProperty = ourProperty; 25 | this.theirProperty = theirProperty; 26 | 27 | fieldname = ourProperty.serializedObject.targetObject.GetPlainType() + "." + ourProperty.GetPlainName(); 28 | 29 | ourInitialValue = ourProperty.GetValue(); 30 | theirInitialValue = theirProperty.GetValue(); 31 | 32 | ourString = SerializedValueString(ourProperty); 33 | theirString = SerializedValueString(theirProperty); 34 | } 35 | 36 | protected override void ApplyOurs() 37 | { 38 | ourProperty.SetValue(ourInitialValue); 39 | ourProperty.serializedObject.ApplyModifiedProperties(); 40 | } 41 | 42 | protected override void ApplyTheirs() 43 | { 44 | var value = theirInitialValue; 45 | 46 | //If we're about references here, get "our" version of the object. 47 | if (ourProperty.propertyType == SerializedPropertyType.ObjectReference) 48 | { 49 | var id = ObjectID.GetFor(theirInitialValue as Object); 50 | var obj = ObjectDictionaries.GetOurObject(id); 51 | 52 | //If we didn't have our own version of the object before, it must be new 53 | if (!obj) 54 | { 55 | //Get our copy of the new object if it exists 56 | obj = ObjectDictionaries.GetOurInstanceOfCopy(value as Object); 57 | } 58 | 59 | value = obj; 60 | } 61 | 62 | ourProperty.SetValue(value); 63 | ourProperty.serializedObject.ApplyModifiedProperties(); 64 | } 65 | 66 | public override void OnGUI() 67 | { 68 | GUILayout.BeginVertical(); 69 | GUILayout.Label(fieldname + ": " + ourProperty.propertyType); 70 | 71 | GUILayout.BeginHorizontal(); 72 | 73 | GUILayout.BeginVertical(); 74 | GUILayout.Label(ourString, GUILayout.Width(100)); 75 | DisplayArray(ourInitialValue); 76 | GUILayout.EndVertical(); 77 | 78 | if (MergeButton(">>>", usingOurs)) 79 | { 80 | UseOurs(); 81 | } 82 | 83 | var c = GUI.backgroundColor; 84 | GUI.backgroundColor = Color.white; 85 | 86 | //GUILayout.Label(ourProperty.propertyType + "/" + ourProperty.type + ": " + ourProperty.GetValue()); 87 | PropertyField(ourProperty); 88 | 89 | GUI.backgroundColor = c; 90 | 91 | if (MergeButton("<<<", usingTheirs)) 92 | { 93 | UseTheirs(); 94 | } 95 | 96 | GUILayout.BeginVertical(); 97 | GUILayout.Label(theirString, GUILayout.Width(100)); 98 | DisplayArray(theirInitialValue); 99 | GUILayout.EndVertical(); 100 | 101 | GUILayout.EndHorizontal(); 102 | GUILayout.EndVertical(); 103 | } 104 | 105 | private void DisplayArray(object value) 106 | { 107 | if (ourProperty.IsRealArray() && ourProperty.isExpanded) 108 | { 109 | var values = (object[])value; 110 | for (int i = 0; i < values.Length; ++i) 111 | { 112 | GUILayout.Label(ValueString(values[i]), GUILayout.Width(100)); 113 | } 114 | } 115 | } 116 | 117 | /// 118 | /// Displays the property field in the center of the window. 119 | /// This method distinguishes between certain properties. 120 | /// The GameObject tag, for example, shouldn't be displayed with a regular string field. 121 | /// 122 | /// The SerializedProerty to display 123 | /// The width of the whole thing in the ui 124 | private void PropertyField(SerializedProperty p, float width = 170) 125 | { 126 | if (p.IsRealArray()) 127 | { 128 | DisplayArrayProperty(p, width); 129 | } 130 | else 131 | { 132 | var oldValue = p.GetValue(); 133 | if (fieldname == "GameObject.TagString") 134 | { 135 | var oldTag = oldValue as string; 136 | var newTag = EditorGUILayout.TagField("", oldTag, GUILayout.Width(width)); 137 | if (newTag != oldTag) 138 | { 139 | p.SetValue(newTag); 140 | } 141 | } 142 | else if (fieldname == "GameObject.StaticEditorFlags") 143 | { 144 | DisplayStaticFlagChooser(p, width); 145 | } 146 | else 147 | { 148 | EditorGUILayout.PropertyField(p, new GUIContent(""), GUILayout.Width(width)); 149 | } 150 | if (!object.Equals(p.GetValue(), oldValue)) 151 | { 152 | p.serializedObject.ApplyModifiedProperties(); 153 | UsedNew(); 154 | } 155 | } 156 | } 157 | 158 | private void DisplayArrayProperty(SerializedProperty p, float width) 159 | { 160 | GUILayout.BeginVertical(); 161 | GUILayout.BeginHorizontal(GUILayout.Width(170)); 162 | EditorGUILayout.PropertyField(p, new GUIContent("Array"), GUILayout.Width(80)); 163 | if (p.isExpanded) 164 | { 165 | var copy = p.Copy(); 166 | var size = copy.arraySize; 167 | 168 | copy.Next(true); 169 | copy.Next(true); 170 | 171 | PropertyField(copy, 70); 172 | GUILayout.EndHorizontal(); 173 | 174 | for (int i = 0; i < size; ++i) 175 | { 176 | copy.Next(false); 177 | PropertyField(copy); 178 | } 179 | } 180 | else 181 | { 182 | GUILayout.EndHorizontal(); 183 | } 184 | GUILayout.EndVertical(); 185 | } 186 | 187 | /// 188 | /// Displays Toggles that let the user set the static flags of the object. 189 | /// 190 | /// The StaticEditorFlags SerializedProperty to display 191 | /// The width of the whole thing in the ui 192 | private void DisplayStaticFlagChooser(SerializedProperty p, float width) 193 | { 194 | var flags = (StaticEditorFlags)p.intValue; 195 | GUILayout.BeginVertical(GUILayout.Width(width)); 196 | 197 | p.isExpanded = EditorGUILayout.Foldout(p.isExpanded, SerializedValueString(p)); 198 | var allOn = true; 199 | if (p.isExpanded) 200 | { 201 | foreach (var flag in System.Enum.GetValues(typeof(StaticEditorFlags)).Cast()) 202 | { 203 | var wasOn = (flags & flag) != 0; 204 | var on = EditorGUILayout.Toggle(flag + "", wasOn); 205 | if (wasOn != on) 206 | { 207 | flags = flags ^ flag; 208 | } 209 | if (!on) 210 | { 211 | allOn = false; 212 | } 213 | } 214 | } 215 | if (allOn) 216 | { 217 | flags = (StaticEditorFlags)(-1); 218 | } 219 | p.intValue = (int)flags; 220 | 221 | GUILayout.EndVertical(); 222 | } 223 | 224 | private string SerializedValueString(SerializedProperty p) 225 | { 226 | if (fieldname == "GameObject.StaticEditorFlags") 227 | { 228 | switch (p.intValue) 229 | { 230 | case 0: 231 | return "Not static"; 232 | case -1: 233 | return "Static"; 234 | default: 235 | return "Mixed static"; 236 | } 237 | } 238 | else if (p.IsRealArray()) 239 | { 240 | return "Array[" + p.arraySize + "]"; 241 | } 242 | return ValueString(p.GetValue()); 243 | } 244 | 245 | private static string ValueString(object o) 246 | { 247 | if (o == null) 248 | { 249 | return "[none]"; 250 | } 251 | return o.ToString(); 252 | } 253 | 254 | private static bool MergeButton(string text, bool green) 255 | { 256 | if (green) 257 | { 258 | GUI.color = Color.green; 259 | } 260 | bool result = GUILayout.Button(text, GUILayout.ExpandWidth(false)); 261 | GUI.color = Color.white; 262 | return result; 263 | } 264 | } 265 | } -------------------------------------------------------------------------------- /Editor/GameObjectMergeActions.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitMerge 3 | { 4 | using UnityEngine; 5 | using UnityEditor; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | 9 | /// 10 | /// One instance of this class represents one GameObject with relevance to the merge process. 11 | /// Holds all MergeActions that can be applied to the GameObject or its Components. 12 | /// Is considered as "merged" when all its MergeActions are "merged". 13 | /// 14 | public class GameObjectMergeActions 15 | { 16 | /// 17 | /// Reference to "our" version of the GameObject. 18 | /// 19 | public GameObject ours { private set; get; } 20 | /// 21 | /// Reference to "their" versoin of the GameObject. 22 | /// 23 | public GameObject theirs { private set; get; } 24 | 25 | public string name { private set; get; } 26 | public bool merged { private set; get; } 27 | public bool hasActions 28 | { 29 | get { return actions.Count > 0; } 30 | } 31 | /// 32 | /// All actions available for solving specific conflicts on the GameObject. 33 | /// 34 | private List actions; 35 | 36 | 37 | public GameObjectMergeActions(GameObject ours, GameObject theirs) 38 | { 39 | actions = new List(); 40 | 41 | this.ours = ours; 42 | this.theirs = theirs; 43 | GenerateName(); 44 | 45 | if (theirs && !ours) 46 | { 47 | actions.Add(new MergeActionNewGameObject(ours, theirs)); 48 | } 49 | else if (ours && !theirs) 50 | { 51 | actions.Add(new MergeActionDeleteGameObject(ours, theirs)); 52 | } 53 | else if (ours && theirs) 54 | { 55 | FindPropertyDifferences(); 56 | FindComponentDifferences(); 57 | } 58 | 59 | // Some Actions have a default and are merged from the beginning. 60 | // If all the others did was to add GameObjects, we're done with merging from the start. 61 | CheckIfMerged(); 62 | } 63 | 64 | /// 65 | /// Generate a title for this object 66 | /// 67 | private void GenerateName() 68 | { 69 | name = ""; 70 | if (ours) 71 | { 72 | name = "Your[" + ours.GetPath() + "]"; 73 | } 74 | if (theirs) 75 | { 76 | if (ours) 77 | { 78 | name += " vs. "; 79 | } 80 | name += "Their[" + theirs.GetPath() + "]"; 81 | } 82 | } 83 | 84 | /// 85 | /// Finds the differences between properties of the two GameObjects. 86 | /// That means the name, layer, tag... everything that's not part of a Component. Also, the parent. 87 | /// 88 | private void FindPropertyDifferences() 89 | { 90 | CheckForDifferentParents(); 91 | FindPropertyDifferences(ours, theirs); 92 | } 93 | 94 | /// 95 | /// Since parenting is quite special, here's some dedicated handling. 96 | /// 97 | private void CheckForDifferentParents() 98 | { 99 | var transform = ours.GetComponent(); 100 | var ourParent = transform.parent; 101 | var theirParent = theirs.GetComponent().parent; 102 | if (!ObjectID.GetFor(ourParent).Equals(ObjectID.GetFor(theirParent))) 103 | { 104 | actions.Add(new MergeActionParenting(transform, ourParent, theirParent)); 105 | } 106 | } 107 | 108 | /// 109 | /// Check for Components that one of the sides doesn't have, and/or for defferent values 110 | /// on Components. 111 | /// 112 | private void FindComponentDifferences() 113 | { 114 | var ourComponents = ours.GetComponents(); 115 | var theirComponents = theirs.GetComponents(); 116 | 117 | // Map "their" Components to their respective ids. 118 | var theirDict = new Dictionary(); 119 | foreach (var theirComponent in theirComponents) 120 | { 121 | // Ignore null components. 122 | if (theirComponent != null) 123 | { 124 | theirDict.Add(ObjectID.GetFor(theirComponent), theirComponent); 125 | } 126 | } 127 | 128 | foreach (var ourComponent in ourComponents) 129 | { 130 | // Ignore null components. 131 | if (ourComponent == null) continue; 132 | 133 | // Try to find "their" equivalent to our Components. 134 | var id = ObjectID.GetFor(ourComponent); 135 | Component theirComponent; 136 | theirDict.TryGetValue(id, out theirComponent); 137 | 138 | if (theirComponent) // Both Components exist. 139 | { 140 | FindPropertyDifferences(ourComponent, theirComponent); 141 | // Remove "their" Component from the dict to only keep those new to us. 142 | theirDict.Remove(id); 143 | } 144 | else 145 | { 146 | // Component doesn't exist in their version, offer a deletion. 147 | actions.Add(new MergeActionDeleteComponent(ours, ourComponent)); 148 | } 149 | } 150 | 151 | // Everything left in the dict is a... 152 | foreach (var theirComponent in theirDict.Values) 153 | { 154 | // ...new Component from them. 155 | actions.Add(new MergeActionNewComponent(ours, theirComponent)); 156 | } 157 | } 158 | 159 | /// 160 | /// Find all the values different in "our" and "their" version of a component. 161 | /// 162 | private void FindPropertyDifferences(Object ourObject, Object theirObject) 163 | { 164 | var ourSerializedObject = new SerializedObject(ourObject); 165 | var theirSerializedObject = new SerializedObject(theirObject); 166 | 167 | var ourProperty = ourSerializedObject.GetIterator(); 168 | if (ourProperty.Next(true)) 169 | { 170 | var theirProperty = theirSerializedObject.GetIterator(); 171 | theirProperty.Next(true); 172 | 173 | var shouldEnterChildren = ourProperty.hasVisibleChildren; 174 | 175 | while (ourProperty.NextVisible(shouldEnterChildren)) 176 | { 177 | theirProperty.NextVisible(shouldEnterChildren); 178 | 179 | // If merging a prefab, ignore the gameobject name. 180 | if (ourObject is GameObject 181 | && MergeManagerBase.isMergingPrefab 182 | && ourProperty.GetPlainName() == "Name") 183 | { 184 | continue; 185 | } 186 | 187 | if (DifferentValues(ourProperty, theirProperty)) 188 | { 189 | // We found a difference, accordingly add a MergeAction. 190 | actions.Add(new MergeActionChangeValues(ours, ourProperty.Copy(), theirProperty.Copy())); 191 | } 192 | } 193 | } 194 | } 195 | 196 | /// 197 | /// Returns true when the two properties have different values, false otherwise. 198 | /// 199 | private static bool DifferentValues(SerializedProperty ourProperty, SerializedProperty theirProperty) 200 | { 201 | if (!ourProperty.IsRealArray()) 202 | { 203 | // Regular single-value property. 204 | return DifferentValuesFlat(ourProperty, theirProperty); 205 | } 206 | else 207 | { 208 | // Array property. 209 | if (ourProperty.arraySize != theirProperty.arraySize) 210 | { 211 | return true; 212 | } 213 | 214 | var op = ourProperty.Copy(); 215 | var tp = theirProperty.Copy(); 216 | 217 | op.Next(true); 218 | op.Next(true); 219 | tp.Next(true); 220 | tp.Next(true); 221 | 222 | for (int i = 0; i < ourProperty.arraySize; ++i) 223 | { 224 | op.Next(false); 225 | tp.Next(false); 226 | 227 | if (DifferentValuesFlat(op, tp)) 228 | { 229 | return true; 230 | } 231 | } 232 | } 233 | 234 | return false; 235 | } 236 | 237 | private static bool DifferentValuesFlat(SerializedProperty ourProperty, SerializedProperty theirProperty) 238 | { 239 | var our = ourProperty.GetValue(); 240 | var their = theirProperty.GetValue(); 241 | 242 | if (ourProperty.propertyType == SerializedPropertyType.ObjectReference) 243 | { 244 | if (our != null && their != null) 245 | { 246 | our = ObjectID.GetFor(our as Object); 247 | their = ObjectID.GetFor(their as Object); 248 | } 249 | } 250 | 251 | return !object.Equals(our, their); 252 | } 253 | 254 | private void CheckIfMerged() 255 | { 256 | merged = actions.TrueForAll(action => action.merged); 257 | } 258 | 259 | /// 260 | /// Use "our" version for all conflicts. 261 | /// This is used on all GameObjectMergeActions objects when the merge is aborted. 262 | /// 263 | public void UseOurs() 264 | { 265 | foreach (var action in actions) 266 | { 267 | action.UseOurs(); 268 | } 269 | merged = true; 270 | } 271 | 272 | /// 273 | /// Use "their" version for all conflicts. 274 | /// 275 | public void UseTheirs() 276 | { 277 | foreach (var action in actions) 278 | { 279 | action.UseTheirs(); 280 | } 281 | merged = true; 282 | } 283 | 284 | //If the foldout is open 285 | private bool open; 286 | public void OnGUI() 287 | { 288 | if (open) 289 | { 290 | GUI.backgroundColor = new Color(0, 0, 0, .8f); 291 | } 292 | else 293 | { 294 | GUI.backgroundColor = merged ? new Color(0, .5f, 0, .8f) : new Color(.5f, 0, 0, .8f); 295 | } 296 | GUILayout.BeginVertical(Resources.styles.mergeActions); 297 | GUI.backgroundColor = Color.white; 298 | 299 | GUILayout.BeginHorizontal(); 300 | open = EditorGUILayout.Foldout(open, new GUIContent(name)); 301 | 302 | if (ours && GUILayout.Button("Focus", EditorStyles.miniButton, GUILayout.Width(100))) 303 | { 304 | // Highlight the instance of the prefab, not the prefab itself. 305 | // Otherwise, "ours". 306 | var objectToHighlight = MergeManagerBase.isMergingPrefab ? MergeManagerPrefab.ourPrefabInstance.GetChildWithEqualPath(ours) : ours; 307 | objectToHighlight.Highlight(); 308 | } 309 | GUILayout.EndHorizontal(); 310 | 311 | if (open) 312 | { 313 | // Display all merge actions. 314 | foreach (var action in actions) 315 | { 316 | if (action.OnGUIMerge()) 317 | { 318 | CheckIfMerged(); 319 | } 320 | } 321 | } 322 | else 323 | { 324 | GUILayout.BeginHorizontal(); 325 | if (GUILayout.Button("Use ours >>>", EditorStyles.miniButton)) 326 | { 327 | UseOurs(); 328 | } 329 | 330 | if (GUILayout.Button("<<< Use theirs", EditorStyles.miniButton)) 331 | { 332 | UseTheirs(); 333 | } 334 | GUILayout.EndHorizontal(); 335 | } 336 | 337 | // If "ours" is null, the GameObject doesn't exist in one of the versions. 338 | // Try to get a reference if the object exists in the current merging state. 339 | // If it exists, the new/gelete MergeAction will have a reference. 340 | if (!ours) 341 | { 342 | foreach (var action in actions) 343 | { 344 | ours = action.ours; 345 | } 346 | } 347 | 348 | GUILayout.EndVertical(); 349 | 350 | GUI.backgroundColor = Color.white; 351 | } 352 | } 353 | } -------------------------------------------------------------------------------- /Editor/GitMergeWindow.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using UnityEngine.SceneManagement; 4 | using System.Linq; 5 | using System.Collections.Generic; 6 | using GitMerge.Utilities; 7 | 8 | namespace GitMerge 9 | { 10 | /// 11 | /// The window that lets you perform merges on scenes and prefabs. 12 | /// 13 | public class GitMergeWindow : EditorWindow 14 | { 15 | private VCS vcs = new VCSGit(); 16 | 17 | private const string EDITOR_PREFS_AUTOMERGE = "GitMerge_automerge"; 18 | private const string EDITOR_PREFS_AUTOFOCUS = "GitMerge_autofocus"; 19 | 20 | public static bool automerge { private set; get; } 21 | public static bool autofocus { private set; get; } 22 | 23 | private MergeManagerBase mergeManager; 24 | 25 | private MergeFilter filter = new MergeFilter(); 26 | private MergeFilterBar filterBar = new MergeFilterBar(); 27 | 28 | public bool mergeInProgress => mergeManager != null; 29 | 30 | private PageView pageView = new PageView(); 31 | private Vector2 scrollPosition = Vector2.zero; 32 | private int tab = 0; 33 | private List mergeActionsFiltered; 34 | 35 | [MenuItem("Window/GitMerge")] 36 | static void OpenEditor() 37 | { 38 | var window = EditorWindow.GetWindow(typeof(GitMergeWindow), false, "GitMerge"); 39 | // In case we're merging and the scene becomes edited, 40 | // the shown SerializedProperties should be repainted 41 | window.autoRepaintOnSceneChange = true; 42 | window.minSize = new Vector2(500, 100); 43 | } 44 | 45 | private void OnEnable() 46 | { 47 | pageView.NumElementsPerPage = 200; 48 | filterBar.filter = filter; 49 | filter.OnChanged += CacheMergeActions; 50 | LoadSettings(); 51 | } 52 | 53 | private static void LoadSettings() 54 | { 55 | automerge = EditorPrefs.GetBool(EDITOR_PREFS_AUTOMERGE, true); 56 | autofocus = EditorPrefs.GetBool(EDITOR_PREFS_AUTOFOCUS, true); 57 | } 58 | 59 | void OnHierarchyChange() 60 | { 61 | // Repaint if we changed the scene 62 | this.Repaint(); 63 | } 64 | 65 | // Always check for editor state changes, and abort the active merge process if needed 66 | private void Update() 67 | { 68 | if (MergeAction.inMergePhase && 69 | (EditorApplication.isCompiling || 70 | EditorApplication.isPlayingOrWillChangePlaymode)) 71 | { 72 | ShowNotification(new GUIContent("Aborting merge due to editor state change.")); 73 | AbortMerge(false); 74 | } 75 | } 76 | 77 | private void AbortMerge(bool showNotification = true) 78 | { 79 | mergeManager.AbortMerge(showNotification); 80 | mergeManager = null; 81 | } 82 | 83 | private void OnGUI() 84 | { 85 | Resources.DrawLogo(); 86 | DrawTabButtons(); 87 | switch (tab) 88 | { 89 | case 0: 90 | OnGUIStartMergeTab(); 91 | break; 92 | 93 | default: 94 | OnGUISettingsTab(); 95 | break; 96 | } 97 | } 98 | 99 | /// 100 | /// Tab that offers scene merging. 101 | /// 102 | private void OnGUIStartMergeTab() 103 | { 104 | if (!mergeInProgress) 105 | { 106 | DisplayPrefabMergeField(); 107 | GUILayout.Space(20); 108 | DisplaySceneMergeButton(); 109 | } 110 | else 111 | { 112 | DisplayMergeProcess(); 113 | } 114 | } 115 | 116 | private void DisplaySceneMergeButton() 117 | { 118 | var activeScene = SceneManager.GetActiveScene(); 119 | 120 | GUILayout.Label("Open Scene: " + activeScene.path); 121 | if (activeScene.path != "" && 122 | !mergeInProgress && 123 | GUILayout.Button("Start merging the open scene", GUILayout.Height(30))) 124 | { 125 | var manager = new MergeManagerScene(this, vcs); 126 | if (manager.TryInitializeMerge()) 127 | { 128 | this.mergeManager = manager; 129 | CacheMergeActions(); 130 | } 131 | } 132 | } 133 | 134 | private void DisplayPrefabMergeField() 135 | { 136 | if (!mergeInProgress) 137 | { 138 | var path = PathDetectingDragAndDropField("Drag a scene or prefab here to start merging", 80); 139 | if (path != null) 140 | { 141 | var asset = AssetDatabase.LoadAssetAtPath(path); 142 | 143 | if (IsPrefabAsset(asset)) 144 | { 145 | var manager = new MergeManagerPrefab(this, vcs); 146 | if (manager.TryInitializeMerge(path)) 147 | { 148 | this.mergeManager = manager; 149 | CacheMergeActions(); 150 | } 151 | } 152 | else if (IsSceneAsset(asset)) 153 | { 154 | var manager = new MergeManagerScene(this, vcs); 155 | if (manager.TryInitializeMerge(path)) 156 | { 157 | this.mergeManager = manager; 158 | CacheMergeActions(); 159 | } 160 | } 161 | } 162 | } 163 | } 164 | 165 | private static bool IsPrefabAsset(Object asset) 166 | { 167 | var assetType = asset.GetType(); 168 | return assetType == typeof(GameObject) || assetType == typeof(BrokenPrefabAsset) || 169 | assetType == typeof(DefaultAsset); 170 | } 171 | 172 | private static bool IsSceneAsset(Object asset) 173 | { 174 | var assetType = asset.GetType(); 175 | return assetType == typeof(SceneAsset); 176 | } 177 | 178 | private static string PathDetectingDragAndDropField(string text, float height) 179 | { 180 | var currentEvent = Event.current; 181 | 182 | using (new GUIBackgroundColor(Color.black)) 183 | { 184 | // Caching these sounds good on paper, but Unity tends to forget them randomly 185 | var content = EditorGUIUtility.IconContent("RectMask2D Icon", string.Empty); 186 | content.text = text; 187 | 188 | var buttonStyle = GUI.skin.GetStyle("Button"); 189 | var style = new GUIStyle(GUI.skin.GetStyle("Box")); 190 | style.stretchWidth = true; 191 | style.normal.background = buttonStyle.normal.background; 192 | style.normal.textColor = buttonStyle.normal.textColor; 193 | style.alignment = TextAnchor.MiddleCenter; 194 | style.imagePosition = ImagePosition.ImageAbove; 195 | 196 | GUILayout.Box(content, style, GUILayout.Height(height)); 197 | } 198 | var rect = GUILayoutUtility.GetLastRect(); 199 | 200 | if (rect.Contains(currentEvent.mousePosition)) 201 | { 202 | if (DragAndDrop.objectReferences.Length == 1) 203 | { 204 | switch (currentEvent.type) 205 | { 206 | case EventType.DragUpdated: 207 | var asset = DragAndDrop.objectReferences[0]; 208 | if (IsPrefabAsset(asset) || IsSceneAsset(asset)) 209 | { 210 | DragAndDrop.visualMode = DragAndDropVisualMode.Move; 211 | } 212 | break; 213 | case EventType.DragPerform: 214 | var path = AssetDatabase.GetAssetPath(DragAndDrop.objectReferences[0]); 215 | DragAndDrop.AcceptDrag(); 216 | return path; 217 | } 218 | } 219 | } 220 | 221 | return null; 222 | } 223 | 224 | /// 225 | /// Tab that offers various settings for the tool. 226 | /// 227 | private void OnGUISettingsTab() 228 | { 229 | var vcsPath = vcs.GetExePath(); 230 | var vcsPathNew = EditorGUILayout.TextField("Path to git.exe", vcsPath); 231 | if (vcsPath != vcsPathNew) 232 | { 233 | vcs.SetPath(vcsPathNew); 234 | } 235 | 236 | automerge = DisplaySettingsToggle(automerge, 237 | EDITOR_PREFS_AUTOMERGE, 238 | "Automerge", 239 | "(Automerge new/deleted GameObjects/Components upon merge start)"); 240 | 241 | autofocus = DisplaySettingsToggle(autofocus, 242 | EDITOR_PREFS_AUTOFOCUS, 243 | "Auto Highlight", 244 | "(Highlight GameObjects when applying a MergeAction to it)"); 245 | } 246 | 247 | private static bool DisplaySettingsToggle(bool value, string editorPrefsKey, string title, string description) 248 | { 249 | var newValue = EditorGUILayout.Toggle(title, value); 250 | if (value != newValue) 251 | { 252 | EditorPrefs.SetBool(editorPrefsKey, value); 253 | } 254 | GUILayout.Label(description); 255 | return newValue; 256 | } 257 | 258 | /// 259 | /// If no merge is in progress, draws the buttons to switch between tabs. 260 | /// Otherwise, draws the "abort merge" button. 261 | /// 262 | private void DrawTabButtons() 263 | { 264 | if (!mergeInProgress) 265 | { 266 | string[] tabs = { "Merge", "Settings" }; 267 | tab = GUI.SelectionGrid(new Rect(72, 36, 300, 22), tab, tabs, 3); 268 | } 269 | else 270 | { 271 | GUI.backgroundColor = new Color(1, 0.4f, 0.4f, 1); 272 | if (GUI.Button(new Rect(72, 36, 300, 22), "Abort merge")) 273 | { 274 | mergeManager.AbortMerge(); 275 | mergeManager = null; 276 | } 277 | GUI.backgroundColor = Color.white; 278 | } 279 | } 280 | 281 | /// 282 | /// Displays all MergeActions and the "apply merge" button if a merge is in progress. 283 | /// 284 | private void DisplayMergeProcess() 285 | { 286 | DrawCommandBar(); 287 | 288 | var done = DisplayMergeActions(); 289 | GUILayout.BeginHorizontal(); 290 | if (done && GUILayout.Button("Apply merge", GUILayout.Height(40))) 291 | { 292 | mergeManager.CompleteMerge(); 293 | mergeManager = null; 294 | } 295 | GUILayout.EndHorizontal(); 296 | } 297 | 298 | /// 299 | /// Display extra commands to simplify merge process 300 | /// 301 | private void DrawCommandBar() 302 | { 303 | DrawQuickMergeSideSelectionCommands(); 304 | filterBar.Draw(); 305 | } 306 | 307 | /// 308 | /// Allow to select easily 'use ours' or 'use theirs' for all actions 309 | /// 310 | private void DrawQuickMergeSideSelectionCommands() 311 | { 312 | GUILayout.BeginHorizontal(); 313 | { 314 | if (GUILayout.Button(new GUIContent("Use ours", "Use theirs for all. Do not apply merge automatically."))) 315 | { 316 | mergeManager.allMergeActions.ForEach((action) => action.UseOurs()); 317 | } 318 | if (GUILayout.Button(new GUIContent("Use theirs", "Use theirs for all. Do not apply merge automatically."))) 319 | { 320 | mergeManager.allMergeActions.ForEach((action) => action.UseTheirs()); 321 | } 322 | GUILayout.FlexibleSpace(); 323 | } 324 | GUILayout.EndHorizontal(); 325 | } 326 | 327 | /// 328 | /// Displays all GameObjectMergeActions. 329 | /// 330 | /// True, if all MergeActions are flagged as "merged". 331 | private bool DisplayMergeActions() 332 | { 333 | var textColor = GUI.skin.label.normal.textColor; 334 | GUI.skin.label.normal.textColor = Color.black; 335 | 336 | bool done = true; 337 | 338 | pageView.Draw(mergeActionsFiltered.Count, (index) => 339 | { 340 | var actions = mergeActionsFiltered[index]; 341 | actions.OnGUI(); 342 | done = done && actions.merged; 343 | }); 344 | 345 | GUI.skin.label.normal.textColor = textColor; 346 | return done; 347 | } 348 | 349 | private void CacheMergeActions() 350 | { 351 | if (filter.useFilter) 352 | { 353 | mergeActionsFiltered = mergeManager.allMergeActions.Where((actions) => filter.IsPassingFilter(actions)).ToList(); 354 | } 355 | else 356 | { 357 | mergeActionsFiltered = mergeManager.allMergeActions; 358 | } 359 | } 360 | } 361 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------