├── .gitignore ├── .npmignore ├── CoverMerge.js ├── PreMerge.js ├── README.md ├── Supporter ├── CreateMark.js ├── Grouping.js ├── IdConverter.js ├── MergePipe.js └── enums.js ├── bin └── index.js ├── licenses └── LICENSE ├── mergeConfig.json ├── package.json ├── readmeRes └── files.jpg ├── singleCoverPart.js └── testHelper.js /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules/ 3 | .vscode/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | .git/ 4 | singleCoverPart.js 5 | testHelper.js 6 | mergeConfig.json -------------------------------------------------------------------------------- /CoverMerge.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const del = require('del'); 4 | const Convert = require('./Supporter/IdConverter'); 5 | 6 | module.exports = { 7 | coverFile: function (tempFile, savePath, name, fileType) { 8 | var merge = fs.readFileSync(tempFile, {encoding: 'utf8'}); 9 | var data = JSON.parse(merge); 10 | var result = this.trans2Normal(data); 11 | 12 | console.log('``````````````finished!````````````````'); 13 | 14 | fs.writeFileSync(`${savePath}/${name}${fileType}`, JSON.stringify(result, null, '\t'), { 15 | encoding: 'utf8', 16 | force: true 17 | }); 18 | del.sync(tempFile, {force: true}); 19 | del.sync(path.join(savePath, 'Mergecache'), {force: true}); 20 | }, 21 | 22 | trans2Normal: function (mergeData) { 23 | var tempData = []; 24 | mergeData = this.sortForTree(mergeData); 25 | for (let i = 0; i < mergeData.length; i++) { 26 | let obj = mergeData[i]; 27 | tempData.push({ 28 | __id__: obj.__id__, 29 | content: obj.content 30 | }); 31 | 32 | this.transComponents(obj, tempData); 33 | this.transPrefabInfos(obj, tempData); 34 | this.transClickEvent(obj, tempData); 35 | } 36 | var con = new Convert(tempData); 37 | var result = con.markToIndex(); 38 | 39 | return result; 40 | }, 41 | 42 | sortForTree: function (mergeData) { 43 | var tempData = []; 44 | tempData.push(mergeData[0]); 45 | 46 | var firstNode = mergeData.find(function (ele) { 47 | if (ele.content.__type__ === 'cc.Scene') 48 | return ele; 49 | }); 50 | // consider about the prefab 51 | if (!firstNode) { 52 | firstNode = mergeData.find(function (ele) { 53 | if (ele.content.__type__ === 'cc.Node') { 54 | return ele; 55 | } 56 | }); 57 | } 58 | this.recurseChild(firstNode, mergeData).forEach(function (obj) { 59 | tempData.push(obj); 60 | }); 61 | 62 | return tempData; 63 | }, 64 | 65 | recurseChild: function (node, mergeData) { 66 | var _self = this; 67 | var record, result = []; 68 | result.push(node); 69 | 70 | if (!node.content._children || node.content._children.length < 0) { 71 | return result; 72 | } 73 | node.content._children.forEach(function (child) { 74 | for (let i = 0; i < mergeData.length; i++) { 75 | if (mergeData[i].__id__ === child.__id__) { 76 | record = _self.recurseChild(mergeData[i], mergeData); 77 | for (let j = 0; j < record.length; j++){ 78 | result.push(record[j]); 79 | } 80 | break; 81 | } 82 | } 83 | }); 84 | 85 | return result; 86 | }, 87 | 88 | transComponents: function (obj, tempData) { 89 | if (!obj._components) { 90 | return; 91 | } 92 | 93 | for (let i = 0; i < obj._components.length; i++) { 94 | obj._components[i].content.node = { 95 | __id__: obj.__id__ 96 | }; 97 | tempData.push({ 98 | __id__: obj._components[i].__id__, 99 | content: obj._components[i].content 100 | }); 101 | obj.content._components.push({ 102 | __id__: obj._components[i].__id__ 103 | }); 104 | } 105 | }, 106 | 107 | transPrefabInfos: function (obj, tempData) { 108 | if (!obj._prefabInfos) { 109 | return; 110 | } 111 | 112 | if (obj._prefabInfos.length > 0) { 113 | obj._prefabInfos[0].content.root = { 114 | __id__: obj.__id__ 115 | };; 116 | tempData.push({ 117 | __id__: obj._prefabInfos[0].__id__, 118 | content: obj._prefabInfos[0].content 119 | }); 120 | obj.content._prefab = { 121 | __id__: obj._prefabInfos[0].__id__ 122 | }; 123 | } 124 | }, 125 | 126 | transClickEvent: function (obj, tempData) { 127 | if (!obj._clickEvent) { 128 | return; 129 | } 130 | 131 | for (let k = 0; k < obj._clickEvent.length; k++) { 132 | tempData.push({ 133 | __id__: obj._clickEvent[k].__id__, 134 | content: obj._clickEvent[k].content 135 | }); 136 | } 137 | }, 138 | } -------------------------------------------------------------------------------- /PreMerge.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const del = require('del'); 5 | const { execFileSync } = require('child_process'); 6 | const cover = require('./CoverMerge'); 7 | const type = require('./Supporter/enums'); 8 | const pipe = require('./Supporter/MergePipe'); 9 | const Convert = require('./Supporter/IdConverter'); 10 | 11 | var config = JSON.parse(fs.readFileSync(`${process.cwd()}/mergeConfig.json`, {encoding: 'utf8'})); 12 | var files = { 13 | base: {}, 14 | local: {}, 15 | remote: {} 16 | }; 17 | 18 | merge = { 19 | start: function (base, local, remote) { 20 | var projectPath = path.parse(base); 21 | var dir = projectPath.dir; 22 | var compareFiles = []; 23 | var merge = path.join(dir, 'merge.json'); 24 | if (projectPath.ext === '.fire' || projectPath.ext === '.prefab') { 25 | files.base = dumpSortFireFiles(base); // base 26 | files.local = dumpSortFireFiles(local); // local 27 | files.remote = dumpSortFireFiles(remote); // remote 28 | // design the path that can be read 29 | if (!fs.existsSync(dir)) { 30 | console.error('Destination path is not available.') 31 | return; 32 | } 33 | // create the compare files, the files ext is the json 34 | compareFiles= outputFiles(dir); 35 | compareForMerge(config.smartMerge.dependMergeTool, compareFiles, merge); 36 | 37 | var name = getFileName(files.base.name); 38 | cover.coverFile(merge, dir, name, projectPath.ext); 39 | } 40 | else { 41 | compareFiles.push(base); 42 | compareFiles.push(local); 43 | compareFiles.push(remote); 44 | 45 | compareForMerge(config.smartMerge.dependMergeTool, compareFiles, merge); 46 | cover.coverFile(merge, dir, name, projectPath.ext); 47 | } 48 | return; 49 | } 50 | } 51 | 52 | function dumpSortFireFiles (originFile) { 53 | var origin = fs.readFileSync(originFile, { 54 | encoding: 'utf8', 55 | }); 56 | var rawData = JSON.parse(origin); 57 | var tempData = []; 58 | var fileProp = path.parse(originFile); 59 | var filesPos = { 60 | name: fileProp.name, 61 | sceneHeader: [], 62 | nodes: [], 63 | components: [], 64 | prefabInfos: [] 65 | } 66 | var con = new Convert(tempData); 67 | resolveData(rawData, tempData); 68 | con.indexToMark(); 69 | groupingData(tempData, filesPos); 70 | 71 | filesPos.nodes.sort(compareByName); 72 | 73 | return filesPos; 74 | } 75 | 76 | function resolveData (rawData, tempData) { 77 | let handler = require('./Supporter/CreateMark'); 78 | for (let i = 0; i < rawData.length; i++) { 79 | switch (rawData[i].__type__) { 80 | case type.prefab: 81 | case type.sceneAsset: 82 | handler.createHeaderId(rawData[i].__type__); 83 | break; 84 | case type.scene: 85 | handler.createSceneId(rawData[i].__type__, rawData[i]._id); 86 | break; 87 | case type.privateNode: 88 | case type.node: 89 | handler.createNodeId(rawData[i].__type__, rawData[i]._id, rawData[i]._name); 90 | break; 91 | case type.prefabInfo: 92 | handler.createPrefabInfo(rawData[i].__type__, rawData[i].fileId); 93 | break; 94 | case type.clickEvent: 95 | handler.createClickEvent(rawData[i].__type__); 96 | break; 97 | default: 98 | handler.createDefault(rawData[i], rawData); 99 | break; 100 | } 101 | 102 | var branch = { 103 | index: i, 104 | name: rawData[i]._name, 105 | type: rawData[i].__type__, 106 | __id__: handler.result.__id__, 107 | _id: handler.result._id, 108 | data: rawData[i] 109 | }; 110 | tempData.push(branch); 111 | } 112 | } 113 | 114 | function groupingData (tempData, filesPos) { 115 | let handler = require('./Supporter/Grouping'); 116 | tempData.forEach(function (obj) { 117 | switch (obj.type) { 118 | case type.scene: 119 | case type.privateNode: 120 | case type.node: 121 | handler.Divide2Nodes(obj, filesPos); 122 | break; 123 | case type.prefabInfo: 124 | handler.Divide2PrefabInfos(obj, filesPos); 125 | break; 126 | case type.sceneAsset: 127 | handler.Divide2SceneAsset(obj, filesPos); 128 | break; 129 | default : 130 | handler.Divide2Components(obj, filesPos); 131 | break; 132 | } 133 | }); 134 | } 135 | // destinationPath is the project root Path 136 | function outputFiles (destinationPath) { 137 | var name = files.base.name; 138 | 139 | var modelBase, modelLocal, modelRemote; 140 | var result = pipe.preReplaceData( 141 | createModel(files.base), 142 | createModel(files.local), 143 | createModel(files.remote), 144 | config.replaceData 145 | ); 146 | if (result) { 147 | modelBase = result[0]; 148 | modelLocal = result[1]; 149 | modelRemote = result[2]; 150 | } 151 | 152 | var compareFold = path.join(destinationPath, '/MergeCache'); 153 | // add the clear the destination fold. 154 | if (fs.existsSync(compareFold)) 155 | del.sync(compareFold + '/**', {force: true}); 156 | 157 | fs.mkdirSync(compareFold); 158 | fs.writeFileSync(compareFold + `/${name}Base.json`, modelBase, { 159 | encoding: 'utf8', 160 | flag: 'w' 161 | }); 162 | fs.writeFileSync(compareFold + `/${name}Local.json`, modelLocal, { 163 | encoding: 'utf8', 164 | flag: 'w' 165 | }); 166 | fs.writeFileSync(compareFold + `/${name}Remote.json`, modelRemote, { 167 | encoding: 'utf8', 168 | flag: 'w' 169 | }); 170 | var paths = fs.readdirSync(compareFold, {encoding: 'utf8'}).map(x => path.join(compareFold, x)); 171 | 172 | return paths; 173 | } 174 | 175 | function createModel (filePos) { 176 | var model = []; 177 | // header 178 | var header = { 179 | __id__: filePos.sceneHeader.__type__, 180 | content: filePos.sceneHeader 181 | }; 182 | model.push(header); 183 | // node 184 | filePos.nodes.forEach(function (obj) { 185 | obj._properties._components = []; 186 | obj._properties._prefab = undefined; 187 | var node = { 188 | __id__: `${obj.__id__}`, 189 | content: obj._properties, 190 | _components: [], 191 | _prefabInfos: [], 192 | _clickEvent: [] 193 | }; 194 | componentModel(node, obj, filePos); 195 | prefabInfoModel(node, obj, filePos); 196 | model.push(node); 197 | }); 198 | 199 | return model; 200 | } 201 | 202 | function compareForMerge (toolPath, compareFiles, merge) { 203 | var base = compareFiles[0]; 204 | var local = compareFiles[1]; 205 | var remote = compareFiles[2]; 206 | 207 | execFileSync(toolPath, [base, local, remote, '-o', merge]); 208 | } 209 | 210 | function componentModel (node, obj, filePos) { 211 | for (let i = 0; i < filePos.components.length; i++) { 212 | var comp = filePos.components[i]; 213 | if (comp._id == obj._id) { 214 | if (comp._properties.__type__ === type.clickEvent) { 215 | node._clickEvent.push({ 216 | __id__: comp.__id__, 217 | content: comp._properties 218 | }); 219 | } 220 | else { 221 | comp._properties.node = undefined; 222 | node._components.push({ 223 | __id__: comp.__id__, 224 | content: comp._properties 225 | }); 226 | } 227 | } 228 | }; 229 | } 230 | 231 | function prefabInfoModel (node, obj, filePos) { 232 | if (!obj.prefab) return; 233 | 234 | for (let i = 0; i < filePos.prefabInfos.length; i++) { 235 | var info = filePos.prefabInfos[i]; 236 | if (obj.prefab.__id__ === info.__id__) { 237 | info._properties.root = undefined; 238 | node._prefabInfos.push({ 239 | __id__: info.__id__, 240 | content: info._properties 241 | }); 242 | break; 243 | } 244 | } 245 | } 246 | 247 | function getFileName (tempName) { 248 | var spell = tempName.split('_'); 249 | var words = []; 250 | for (let i = 0; i < spell.length; i++) { 251 | if (spell[i] === 'BASE') { 252 | var name = words.join('_'); 253 | return name; 254 | } 255 | words.push(spell[i]); 256 | } 257 | } 258 | 259 | function compareByName (a, b) { 260 | return a.__id__.localeCompare(b.__id__); 261 | } 262 | 263 | module.exports = merge; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Creator Smart-Merge Tool 2 | 3 | ### 为什么设计这个工具? 4 | 5 | 这个工具提供给广大 Cocos Creator 开发者用于合并不同版本或相同版本下的场景内容修改分歧。对于开发者来说,希望将不同修改的场景数据融合是十分困难的一件事情,因为目前 Cocos Creator 场景文件的数据存储结构对于融合这一概念并不友好(想必很多用户已经体验过了)。为解决这一难题,从而制作了这个简陋的小工具。 6 | 工具本身内容不多,如果有任何问题,建议有想法的你在文中的 git 仓库链接中提交你的想法。
7 | https://github.com/cocos-creator/smart-merge-tool/issues 8 | 9 | ### 针对人群: 10 | 11 | 当前这个工具仍然处于非常初级的阶段,对于非程序员而言,调试配置过程可能相对困难,因此建议是有一定工具使用经验的 __程序员__ 来操作这个工具。 12 | 13 | ### 包含的功能: 14 | 15 | - 合并 .fire 后缀的场景文件。 16 | - 合并 .prefab 为后缀的预制体文件。 17 | 18 | ### 合并原理: 19 | 20 | 这个工具并不是独立存在的集成工具,而是需要依赖于项目管理工具(例如:SourceTree) 以及合并冲突解决工具(例如:KDiff3)这两类工具存在的。 21 | 这里重点强调一下合并冲突解决工具,这一类工具属于半自动化工具,可以自动识别你当前文件的冲突内容并且自动的解决这部分简单的冲突,但是有部分的冲突超出了它的能力范围,你需要 __手动__ 的对这些冲突进行选择并且合并。
22 | Creator Smart-Merge(以下简称 __CSM__)工具首先通过识别你当前使用的合并工具,在解决冲突时产生的 BASE, LOCAL, REMOTE 三个文件(如下图),然后将三个文件相关内容数据进行重新排序。 23 | ![files](readmeRes/files.jpg) 24 | 25 | 以上操作,CSM 会调用用户在工程下创建的 mergeConfig.json 文件,通过识别用户配置数据,调用用户习惯使用的第三方冲突解决工具。在用户手动的解决所有期望的调整之后,工具会自动的生成一个新的文件覆盖你当前的冲突场景文件。
26 | 在合并过程中会有一个 MergeCache 文件夹和一个 merge.json 文件生成,MergeCache 文件夹下是三个经过数据处理对应场景文件的 json 文件。 merge.json 文件是通过第三方冲突解决工具保存生成的合并文件。 27 | 28 | ### 合并冲突解决工具配置流程: 29 | 30 | 1. 安装合并工具到全局设置。这样你就可以使用 __merge__ 开头的命令操作了。 31 | ``` 32 | npm install -g creator-smart-merge 33 | ``` 34 | 2. 配置你的版本管理工具(目前只支持 SourceTree),点击 Tools -> Options -> Diff 设置 MergeTool 为 Custom, 35 | 之后配置 __Merge Command__: 36 | ``` 37 | merge 38 | ``` 39 | __Arguments__ 传入参数设置为: 40 | 41 | ``` 42 | start $BASE $LOCAL $REMOTE 43 | ``` 44 | 3. 在当前的工程文件中新建一个 mergeConfig.json 文件,内容配置为: 45 | ``` 46 | { 47 | // 冲突解决工具 48 | "smartMerge": { 49 | "dependMergeTool": "D:/KDiff3/KDiff3.exe" 50 | }, 51 | // 过滤可自动处理的选项 52 | "replaceData": { 53 | "dataType": [], 54 | "dataName": [], 55 | "branch": [], 56 | "isReplace": false 57 | } 58 | } 59 | ``` 60 | - smartMerge.dependMergeTool: 工具为你当前电脑上常用冲突解决工具,目前因为工具较为简陋,支持最好的是 KDiff3 工具,建议使用,当然如果你对工具的使用较为了解,也可以自行针对 CSM 的代码进行简单的调整用于适应你的工具。 61 | - replaceData: 是额外的辅助功能部分,仅针对场景或者预制体(prefab)__节点__ 相同但是数据不同的合并冲突有效,因为无法做到对不同节点数量的数据排序以及合并内容的完全一一对应,所以这个功能仅仅是帮助你减少一部分的工作量。 62 | - dataType:数据类型可选类型为: "Node", "Component", "PrefabInfos", "ClickEvent"。 63 | - dataName:数据名称,例如:"_name", "_children"... 选取为当前数据类型下content 结构中含有的数据成分。 64 | - branch: 替换数据的分支,三个选择:"Base", "Local", "Remote"。 65 | - isReplace:是否进行替换,false 时将直接将排序后的文件返回给你。 66 | 67 | ### 操作流程: 68 | 69 | #### 场景合并: 70 | 71 | ##### 同版本合并: 72 | 73 | 同一版本的合并较为简单,当冲突产生时,只要按照上方 “合并冲突解决工具配置流程” 的方案设置好内容之后在 SourceTree 下右键点击冲突文件内容 Resolve Conflicts -> Execute External Merge Tool,就可以直接运行当前设置的工具了。 74 | 75 | ##### 跨版本合并: 76 | 77 | 随着 Cocos Creator 的不断发展,2.0 版本的编辑器以及引擎已经推出,这个新版本针对文件结构进行了大量的修改,因此在合并场景过程中,我并 __不建议__ 您去合并不同版本的场景,那样会带来大量不可预计的错误,同时合并的难度比起相同版本下场景合并也会加大。 78 | 但是如果你有刚需,那么这里也提供以下这种合并的方案。 __强烈建议合并场景前请一定要保存好备份__ 79 | 80 | Creator 针对 1.0 到 2.0 分别采用的不同的数据模式,2.0 的数据更加的丰富与完善,因此 1.0 版本场景在合并到 2.0 版本过程中会有更多的问题,在此仅提供一个项目开发过程中实现的办法。 81 | 首先你需要将 SourceTree 上文中提到的 diff tool 设置成为你当前常用的冲突解决工具(如:KDiff3)这个时候当你开始合并时,SourceTree 会主动调用那个工具,并且生成三个冲突文件,你的任务就是配置好 testHelper.js (这里下文会有介绍)的调用方法之后,先通过 __2.0 版本__ 的编辑器,依次打开三个场景文件并保存,将三个冲突的场景文件数据都升级到 2.0 版,在这个基础上通过调用 testHelper.js 直接开始合并你的场景。 82 | 合并完成之后如上文所说合并完成的场景文件会覆盖原场景文件,这个时候场景文件可以在编辑器内打开了。你要通过复制的方式将文件 copy 一份在本地,之后点击 SourceTree 的 Abort 放弃本次冲突解决,因为你没有使用正常流程解决冲突,SourceTree 会帮你把场景文件恢复成冲突状态,这个时候你将 copy 的完成式文件覆盖这个冲突场景文件,然后直接保存到提交中就 ok 了。
83 | 跨版本冲突解决比较麻烦,如果觉得文档有什么不清楚的地方可以到文中链接地址处提交 issue 进行交流。 84 | 85 | #### 合并完成时: 86 | 87 | 建议合并完成之后进入对应版本编辑器查看场景是否可以打开,因为冲突场景在编辑器内是无法开启的。确认无误之后进行 Commit 提交。 88 | 89 | #### 因为意外中止了合并: 90 | 91 | SourceTree 这一类的版本管理工具不同于命令行会显示详细的报错信息,因此您可能在合并过程以及结束时遇到一些问题,以下假设几个情景可能会帮助你解决遇到的问题。 92 | 93 | ##### 情景一: 94 | 95 | 成功的合并了三个文件生成了 merge.json 文件但是却没有覆盖冲突场景。 96 | 这很有可能是 merge.json 文件出了问题,因为 json 文件对格式要求比较高,在大型多节点场景中难免会有格式或者数据错误,这时候文本无法正确识别 json 文件内容,就会产生中断报错。
97 | 解决方案: 98 | 1. 重新合并一次。 99 | 2. 如格式问题比较简单,并且确认其他数据正常的情况下,建议手动修改 json 文件,再调用额外工具中 singleCoverPart.js 文件,直接进行场景文件转换。 100 | 101 | ##### 情景二: 102 | 103 | 成功生成并覆盖了原有场景,但是编辑器无法打开场景或者场景内节点丢失,组件丢失。 104 | 这属于合并场景中常见的问题,因为新的场景文件是根据 \_\_id\_\_ 索引 寻找对应数据信息的,如果合并中没有选取同分支数据对象,在 json 转换 fire 文件过程中就无法正确的获得数据信息。 105 | 解决方案: 106 | 1. 检查相关节点引用 id 重新进行合并。因为丢失的节点是不会录入新的场景文件中的。 107 | 108 | ### 额外的脚本工具: 109 | 110 | 首先从以下网址中将工具内所有的脚本下载下来(这其实就是这个工具的工作原理):https://github.com/cocos-creator/smart-merge-tool
111 | 顺便建议采用 vsc 进行工具调试。 这里有两个特殊调试文件可以供你来使用 testHelper.js 和 singleCoverPart.js 当然这两个脚本因为是我个人调试用的,所以配置起来可能会有点麻烦。 112 | 那么来介绍一下这两个脚本吧, singleCoverPart.js 是当你合并场景失败了但是已经产生了 merge.json 文件时,merge.json 文件内容中有部分错误,但是错误仍然在可手动处理范围内(这个是否可处理,就是看你个人的理解能力了)你可以手动的处理这些低级的错误之后配置 launch.json 文件或者直接使用命令行调用命令(我个人更建议配置 launch.json): 113 | ``` 114 | node singleCoverPart.js /you/program/dir/ fileName 115 | ``` 116 | 117 | 传入两个参数,你工程的绝对路径以及需要替换的场景名称。 118 | 这样可以直接调用 coverMerge.js 脚本合并部分的功能,使你不必重新合并大量的数据。 119 | testHelper.js 脚本用于调用 bin 目录下的 index.js 文件,一样需要你配置一下 vsc 的 launch.json 或者使用 cmd 直接调用。调用命令为: 120 | ``` 121 | node testHelper.js start path fileName.fire/.prefab 122 | ``` 123 | 124 | path 参数为项目当前冲突文件所在位置的绝对路径, 125 | fileName 传入不是当前文件名称。而是 SourceTree 合并工具生成的 BASE 分支文件,文件名称举例为: SceneName_BASE_123456.fire 这种类型。(至于为什么是这种文件名称和上方的解决冲突方案相关。)
126 | 127 | ### 生成的 json 文件数据结构分析: 128 | 129 | ``` 130 | { 131 | // 当前生成 json 文件的对比格式。 132 | "__id__": "cc.Node: Background, id: 0a930RkW5pOkKgXYzQG0cOj", 133 | "content": {...}, 134 | "_components": [], 135 | "_prefabInfos": [], 136 | "_clickEvent": [] 137 | } 138 | ``` 139 | - \_\_id\_\_:是一个由代码生成的数据唯一标识。合并工具会根据这个唯一标识对数据内容的索引进行位置确认。生成标识的结构针对不同的数据类型会有细微的变化。
140 | - content:为 fire 文件内的原数据内容,但是为了更好的合并 cc.Node 类型去除了 components, prefab 两个数据的索引信息。
141 | - _prefabInfos:当前节点的 prefab 信息。
142 | - _components:是当前节点的绑定组件内容。
143 | - _clickEvent: 当前点击事件的绑定对象数据。 144 | 145 | #### \_\_id\_\_ 标识 146 | 147 | ``` 148 | ${_type: String}: ${name: String}, id: ${_id: String}, [index: Number] 149 | ``` 150 | 151 | 以上为标识识别格式。 SceneAsset、Scene、Node、PrivateNode、Component、CustomeEvent、ClickEvent 都会生成一个类似标识,用于帮助你在合并排查时确认是否是同一对象。 152 | 153 | 154 | ### 存在无法解决的问题: 155 | 156 | - 比较特殊的冲突问题是针对 _id 数据的比较,目前没有特别好的办法调整。
157 | 举例而言,当一个版本场景下增添了一个节点数据,在另一个版本下并没有通过合并的方式修改场景,而是通过手动增添一个同样的节点数据在场景中,这会导致相同的节点数据信息但是拥有不同的 _id 数据,因为新生成的节点都会有相应的唯一 _id 生成。最终,不同的 _id 在对比冲突的过程中会干扰排序,比较,融合等一系列操作。
158 | - 冲突对比过程中,以上结构尽量减少了 \_\_id\_\_ 的存在,想要通过索引信息查找需求对应的数据会比较困难。 -------------------------------------------------------------------------------- /Supporter/CreateMark.js: -------------------------------------------------------------------------------- 1 | const type = require('../Supporter/enums'); 2 | 3 | var createMark = { 4 | // return back 5 | result: { 6 | __id__: '', 7 | _id: '' 8 | }, 9 | // record the component name if there are some components as same 10 | compAssemblyData: {}, 11 | nodeAssemblyData: {}, 12 | 13 | createHeaderId: function (_type) { 14 | this.result.__id__ = `${_type}: fileHeader`; 15 | this.result._id = ''; 16 | }, 17 | 18 | createSceneId: function (_type, _id) { 19 | this.result.__id__ = `${_type}: Scene, id: ${_id}`; 20 | this.result._id = ''; 21 | }, 22 | 23 | createNodeId: function (_type, _id, name) { 24 | if (!_id) { 25 | var member = `${_type}: ${name}`; 26 | if (Object.keys(this.nodeAssemblyData).includes(member) > 0) { 27 | this.nodeAssemblyData[member] += 1; 28 | } 29 | else { 30 | this.nodeAssemblyData[member] = 1; 31 | } 32 | _id = this.nodeAssemblyData[member]; 33 | } 34 | this.result.__id__ = `${_type}: ${name}, id: ${_id}`; 35 | this.result._id = _id; 36 | }, 37 | 38 | createPrefabInfo: function (_type, fileId) { 39 | this.result.__id__ = `${_type}: ${fileId}`; 40 | this.result._id = ''; 41 | }, 42 | 43 | createClickEvent: function (_type) { 44 | this.result.__id__ = `${_type}`; 45 | this.result._id = ''; 46 | }, 47 | 48 | createComponent: function (node, _type) { 49 | this.result.__id__ = `${type.comp}: ${_type}, Node: ${node._name}(${node._id})`; 50 | if (Object.keys(this.compAssemblyData).includes(this.result.__id__) > 0) { 51 | this.compAssemblyData[this.result.__id__] += 1; 52 | this.result.__id__ = `${type.comp}: ${_type}, Node: ${node._name}(${node._id}), index: ${this.compAssemblyData[this.result.__id__]}`; 53 | } 54 | else { 55 | this.compAssemblyData[this.result.__id__] = 0; 56 | } 57 | this.result._id = node._id; 58 | }, 59 | 60 | createCustemEvent: function () { 61 | this.result.__id__ = type.custom; 62 | if (Object.keys(this.compAssemblyData).includes(this.result.__id__) > 0) { 63 | this.compAssemblyData[this.result.__id__] += 1; 64 | this.result.__id__ = `${type.custom}, index: ${this.compAssemblyData[this.result.__id__]}`; 65 | } 66 | else { 67 | this.compAssemblyData[this.result.__id__] = 0; 68 | } 69 | this.result._id = ''; 70 | }, 71 | 72 | createDefault: function (target, rawData) { 73 | if (target.node) { 74 | var nodeIndex = target.node.__id__; 75 | this.createComponent(rawData[nodeIndex], target.__type__); 76 | } 77 | else { 78 | this.createCustemEvent(); 79 | } 80 | } 81 | } 82 | 83 | module.exports = createMark; -------------------------------------------------------------------------------- /Supporter/Grouping.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Divide2Nodes: function (obj, filesPos) { 3 | var node = { 4 | _id: obj.data._id, 5 | prefab: obj.data._prefab, 6 | __id__: obj.__id__, 7 | _properties: obj.data 8 | }; 9 | filesPos.nodes.push(node); 10 | }, 11 | 12 | Divide2PrefabInfos: function (obj, filesPos) { 13 | var info = { 14 | __id__: obj.__id__, 15 | _properties: obj.data 16 | } 17 | filesPos.prefabInfos.push(info); 18 | }, 19 | 20 | Divide2SceneAsset: function (obj, filesPos) { 21 | filesPos.sceneHeader = obj.data; 22 | }, 23 | 24 | Divide2Components: function (obj, filesPos) { 25 | var node = ''; 26 | if (obj.data.node) { 27 | node = obj.data.node.__id__; 28 | } 29 | var component = { 30 | node: node, 31 | __id__: obj.__id__, 32 | // _id is belong to node 33 | _id: obj._id, 34 | _properties: obj.data 35 | }; 36 | filesPos.components.push(component); 37 | } 38 | } -------------------------------------------------------------------------------- /Supporter/IdConverter.js: -------------------------------------------------------------------------------- 1 | const type = require('./enums'); 2 | 3 | var convert = function (tempData) { 4 | this.tempData = tempData; 5 | } 6 | 7 | var proto = convert.prototype; 8 | 9 | proto.indexToMark = function () { 10 | for (let i = 0; i < this.tempData.length; i++) { 11 | var obj = this.tempData[i]; 12 | obj.data = this.locationId(obj); 13 | } 14 | } 15 | 16 | proto.locationId = function (obj) { 17 | var str = JSON.stringify(obj.data, null, '\t'); 18 | str = str.replace(/"__id__": ([0-9]+)/g, (match, index) => { 19 | var __id__ = this.replaceMarkFun(index, obj); 20 | return `"__id__": "${__id__}"`; 21 | }); 22 | obj.data = JSON.parse(str); 23 | 24 | return obj.data; 25 | } 26 | 27 | proto.markToIndex = function () { 28 | var data = []; 29 | for (let i = 0; i < this.tempData.length; i++) { 30 | var obj = this.tempData[i]; 31 | obj = this.locationIndex(obj.content); 32 | data.push(obj); 33 | } 34 | 35 | return data; 36 | } 37 | 38 | proto.replaceMarkFun = function (index, obj) { 39 | var __id__ = ''; 40 | var target = this.tempData[index]; 41 | var _id = obj._id; 42 | var _name = obj.name; 43 | if (target.data.__type__ === type.clickEvent) { 44 | __id__ = `${type.clickEvent}: ${target.data.handler}, Comp ${_name}(${_id})`; 45 | target.__id__ = __id__; 46 | target._id = _id; 47 | } 48 | else if (target.__id__ && target.__id__.includes(type.custom)) { 49 | __id__ = this.getMark(this.tempData, parseInt(index)); 50 | target._id = _id; 51 | } 52 | else { 53 | __id__ = this.getMark(this.tempData, parseInt(index)); 54 | } 55 | return __id__; 56 | } 57 | 58 | proto.locationIndex = function (objData) { 59 | // must the node sort first, or will lost the __id__ 60 | var str = JSON.stringify(objData, null, '\t'); 61 | str = str.replace(/"__id__": "([\S ]+)"/g, (match, __id__) => { 62 | var index = this.replaceIndexFun(__id__); 63 | return `"__id__": ${index}`; 64 | }); 65 | objData = JSON.parse(str); 66 | 67 | return objData; 68 | } 69 | 70 | proto.replaceIndexFun = function (__id__) { 71 | var index = this.tempData.findIndex(function (ele) { 72 | if (`"${__id__}"` === JSON.stringify(ele.__id__)) { 73 | return ele; 74 | } 75 | }); 76 | return index; 77 | } 78 | 79 | proto.getMark = function (array, index) { 80 | var obj = array.find(function (ele) { 81 | if (ele.index === index) { 82 | return ele; 83 | } 84 | }); 85 | 86 | return obj.__id__; 87 | } 88 | 89 | 90 | 91 | module.exports = convert; -------------------------------------------------------------------------------- /Supporter/MergePipe.js: -------------------------------------------------------------------------------- 1 | var pipe = { 2 | base: [], 3 | local: [], 4 | remote: [], 5 | } 6 | 7 | function replaceData (type, dataName, branch) { 8 | var mainData = choiceMainBranch(branch); 9 | 10 | if (type === 'Node') { 11 | partOfNode(mainData, dataName); 12 | } 13 | else { 14 | partOfDefault(mainData, dataName, type); 15 | } 16 | } 17 | /** 18 | * only replace the data. 19 | * @param {Array} tempData - currect target to be replaced by the data 20 | * @param {Object} mainData - as the replacement data 21 | * @param {Object} mainData.content - the whole data content about the data 22 | * @param {String} mainData.__id__ - the uniquely identifies 23 | * @param {Number} index - locate the same position target 24 | * @param {String} dataName - prototype 25 | */ 26 | function replaceContent (tempData, mainData, index, dataName) { 27 | if (checkContentReplaceable(tempData[index], mainData, dataName)) { 28 | tempData[index].content[dataName] = mainData.content[dataName]; 29 | } 30 | } 31 | 32 | function replaceId (tempData, mainData, index) { 33 | if (checkIdReplaceable(tempData[index], mainData)) { 34 | tempData[index].__id__ = mainData.__id__; 35 | } 36 | } 37 | 38 | function partOfNode (mainData, dataName) { 39 | mainData[0].forEach(function (data, index) { 40 | if (!data.content[dataName]) { 41 | if (dataName === '__id__') { 42 | mainData[1] && replaceId(mainData[1], data, index); 43 | mainData[2] && replaceId(mainData[2], data, index); 44 | } 45 | return; 46 | } 47 | mainData[1] && replaceContent(mainData[1], data, index, dataName); 48 | mainData[2] && replaceContent(mainData[2], data, index, dataName); 49 | }); 50 | } 51 | // this part contain component, prefabInfo, clickEvent, custome 52 | function partOfDefault (mainData, dataName, type) { 53 | mainData[0].forEach(function (data, index1) { 54 | if (!data[type]) { 55 | return; 56 | } 57 | data[type].forEach(function (obj, index2) { 58 | var data1 = mainData[1][index1]; 59 | var data2 = mainData[2][index1]; 60 | if (!obj.content[dataName]) { 61 | if (dataName === '__id__') { 62 | data1 && replaceId(data1[type], obj, index2); 63 | data2 && replaceId(data2[type], obj, index2); 64 | } 65 | return; 66 | } 67 | data1 && replaceContent(data1[type], obj, index2, dataName); 68 | data2 && replaceContent(data2[type], obj, index2, dataName); 69 | }); 70 | }); 71 | } 72 | 73 | // return a array and the main branch will set at the first. 74 | function choiceMainBranch (branch) { 75 | var dataArray = []; 76 | switch (branch) { 77 | case "Base": 78 | dataArray.push(pipe.base); 79 | dataArray.push(pipe.local); 80 | dataArray.push(pipe.remote); 81 | break; 82 | case "Local": 83 | dataArray.push(pipe.local); 84 | dataArray.push(pipe.base); 85 | dataArray.push(pipe.remote); 86 | break; 87 | case "Remote": 88 | dataArray.push(pipe.remote); 89 | dataArray.push(pipe.base); 90 | dataArray.push(pipe.local); 91 | break; 92 | default: 93 | throw "Branch choice error."; 94 | } 95 | 96 | return dataArray; 97 | } 98 | 99 | function checkContentReplaceable (tempData, mainData, dataName) { 100 | if (!tempData) { 101 | return false; 102 | } 103 | // check the tempData if the tempData is the same as the mainData 104 | if (tempData.__id__ === mainData.__id__) { 105 | return true; 106 | } 107 | else if (tempData.content.__type__ === mainData.content.__type__) { 108 | if (tempData.content._name === mainData.content._name) { 109 | // did not have the prototype 110 | if (!tempData.content[dataName]) { 111 | return false; 112 | } 113 | return true; 114 | } 115 | } 116 | 117 | return false; 118 | } 119 | 120 | function checkIdReplaceable (tempData, mainData) { 121 | if (!tempData) { 122 | return false; 123 | } 124 | if (tempData.content.__type__ !== mainData.content.__type__) { 125 | return false; 126 | } 127 | // only use to change the _id part 128 | if (tempData.content._name !== mainData.content._name) { 129 | return false; 130 | } 131 | 132 | return true; 133 | } 134 | 135 | /** 136 | * @param {Array} branchData - It is currect branch model data. 137 | * @param {Object} config - It is the config that you want to replace branch. 138 | * @param {String} config.dataType - It is the data type that can help you find the target exactly. 139 | * @param {String} config.dataName - It is the prototype name that you want to replace. 140 | * @param {String} config.branch - It is the branch that you choice as the main branch to replace others branch data. 141 | * @param {Boolean} config.isReplace - If you want to replace the data. 142 | */ 143 | pipe.preReplaceData = function preReplaceTheDataToCutDownTheMergeConflict (baseData, localData, remoteData, config) { 144 | var result = []; 145 | this.base = baseData; 146 | this.local = localData; 147 | this.remote = remoteData; 148 | 149 | if (config.isReplace) { 150 | config.dataName.forEach(function (name, index) { 151 | var type = config.dataType[index]; 152 | var branch = config.branch[index]; 153 | switch(type) { 154 | case "Node": 155 | replaceData('Node', name, branch); 156 | break; 157 | case "Component": 158 | replaceData('_components', name, branch); 159 | break; 160 | case "PrefabInfos": 161 | replaceData('_prefabInfos', name, branch); 162 | break; 163 | case "ClickEvent": 164 | replaceData('_clickEvent', name, branch); 165 | break; 166 | }; 167 | }); 168 | } 169 | result.push(this.base); 170 | result.push(this.local); 171 | result.push(this.remote); 172 | 173 | return result.map( x => x = JSON.stringify(x, null, '\t')); 174 | } 175 | 176 | module.exports = pipe; -------------------------------------------------------------------------------- /Supporter/enums.js: -------------------------------------------------------------------------------- 1 | const typeEnums = { 2 | node: 'cc.Node', 3 | sceneAsset: 'cc.SceneAsset', 4 | scene: 'cc.Scene', 5 | prefab: 'cc.Prefab', 6 | privateNode: 'cc.PrivateNode', 7 | prefabInfo: 'cc.PrefabInfo', 8 | clickEvent: 'cc.ClickEvent', 9 | // special part, is design by custom 10 | comp: 'Comp', 11 | custom: 'CustomTarget' 12 | }; 13 | 14 | module.exports = typeEnums; 15 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const program = require('commander'); 3 | const preMerge = require('../PreMerge'); 4 | 5 | program 6 | .command('start ') 7 | .description('start merge file') 8 | .action (function (base, local, remote) { 9 | preMerge.start(base, local, remote); 10 | }); 11 | 12 | program.parse(process.argv); -------------------------------------------------------------------------------- /licenses/LICENSE: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. 3 | 4 | http://www.cocos.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated engine source code (the "Software"), a limited, 8 | worldwide, royalty-free, non-assignable, revocable and non-exclusive license 9 | to use Cocos Creator solely to develop games on your target platforms. You shall 10 | not use Cocos Creator software for developing other software or tools that's 11 | used for developing games. You are not granted to publish, distribute, 12 | sublicense, and/or sell copies of Cocos Creator. 13 | 14 | The software or tools in this License Agreement are licensed, not sold. 15 | Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | ****************************************************************************/ -------------------------------------------------------------------------------- /mergeConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "smartMerge": { 3 | "dependMergeTool": "D:/KDiff3/kdiff3.exe" 4 | }, 5 | "replaceData": { 6 | "dataType": [], 7 | "dataName": [], 8 | "branch": [], 9 | "isReplace": false 10 | } 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "creator-smart-merge", 3 | "version": "1.0.4", 4 | "description": "Cocos Creator expand plugin help user to merge their .fire/.prefab files.", 5 | "main": "merge.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:cocos-creator/smart-merge-tool.git" 9 | }, 10 | "bin": { 11 | "merge": "./bin/index.js" 12 | }, 13 | "author": "ColinCollins", 14 | "license": { 15 | "type" : "Cocos", 16 | "url" : "./licenses/LICENSE" 17 | }, 18 | "devDependencies": { 19 | "commander": "^2.16.0", 20 | "del": "^3.0.0" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/cocos-creator/smart-merge-tool/issues" 24 | }, 25 | "readmeFilename": "README.md" 26 | } 27 | -------------------------------------------------------------------------------- /readmeRes/files.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocos-creator/smart-merge-tool-deprecated/18e66eda940e837750a2fd5f167ecb4f42d5d3f5/readmeRes/files.jpg -------------------------------------------------------------------------------- /singleCoverPart.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | const path = require('path'); 3 | const cover = require('./coverMerge'); 4 | 5 | (function () { 6 | var args = process.argv; 7 | if (args.length < 3) { 8 | console.error('Arguments not enough!'); 9 | return; 10 | } 11 | 12 | var projectPath = path.parse(args[2]); 13 | var dir = projectPath.dir; 14 | var merge = path.join(dir, 'merge.json'); 15 | var name = args[3]; 16 | 17 | cover.coverFile(merge, dir, name); 18 | })(); -------------------------------------------------------------------------------- /testHelper.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const process = require('child_process'); 3 | 4 | (function () { 5 | var launch = fs.readFileSync('.vscode/launch.json',{encoding: 'utf8'}); 6 | var parse = JSON.parse(launch); 7 | let args = parse.configurations[0].args; 8 | let fileParse = args[1].match(/([\w-]+)_BASE_([0-9]+)([\.\w]+)/); 9 | var base = `${args[0]}/${fileParse[1]}_BASE_${fileParse[2]}${fileParse[3]}`; 10 | var local = `${args[0]}/${fileParse[1]}_LOCAl_${fileParse[2]}${fileParse[3]}` 11 | var remote = `${args[0]}/${fileParse[1]}_REMOTE_${fileParse[2]}${fileParse[3]}` 12 | const { cwd } = require('process'); 13 | var child = process.spawn('node', ['bin/index.js', 'start', base, local, remote], { 14 | cwd: cwd(), 15 | env: process.env 16 | }); 17 | child.stderr.on('data', (data) => { 18 | console.log(`stderr: ${data}`) 19 | }); 20 | 21 | child.stdout.on('data', (data) => { 22 | console.log(`stdout: ${data}`); 23 | }); 24 | 25 | child.on('close', (code) => { 26 | console.log(`child process exited with code: ${code}`); 27 | }); 28 | })(); --------------------------------------------------------------------------------