解决iOS项目文件合并.xcodeproj冲突
# 前言
用 Xcode 做 iOS 开发的同学都知道,在项目根目录里有一个 project.xcodeproj 文件。如果多人协作,项目合并的时候这个文件非常容易有冲突。一旦这个文件冲突了项目就打不开了,然后就非常不情愿地用文本编辑器打开这个文件,人肉搜索 >>> <<< 或者 === 来进行人肉替换解决冲突问题。
这简直就是浪费青春,如果合并的是一个距离上次 commit 已经很了久的崭新的 commit,你估计会找同事“聊聊人生”... 怎么解决这个问题呢?
# .xcodeproj
项目 .xcodeproj 文件夹底下一般有4个文件:
- project.pbxproj 文件
- xcuserdata 文件夹
- xcshareddata 文件夹
- project.xcworkspace 文件夹
# xcuserdata / xcshareddata
存放用户相关的文件,包含 user state,folders 的状态,最后打开的文件等。一般来说是不需要提交的也不会冲突的。
# project.xcworkspace
workspace 是一种 Xcode documentation,可以将多个 project 和其它文件放到一起,这样可以 work on them together。一个 project 也可以属于多个 workspace。所以简单来讲,workspace 里面就是一个或多个 projects 的 reference,放在一起,有时候比较好工作。也包含一些用户的断点设置信息。
如果项目里面根本就没有 workspace 的概念,或者只有一个 workspace + 一个 project 时,这个 workspace 并不会有什么变动,那么这个文件夹可以忽略,更不会出现冲突问题。如果 project 很依赖 workspace,没有 workspace 就运行不了,虽然不能忽略这个文件夹,但也不容易出现冲突。出现冲突最多的是下面的 project.pbxproj 文件。
# project.pbxproj
而 project.pbxproj 这个文件被称之为“梦魇”,是一个老式的 plist,记录整个项目的层次结构,包含了所有此项目 build 需要的元数据,setting、file reference、configuration、targets 等等。也就是说,这个文件代表的就是这个 project。
/* Begin PBXBuildFile section */
351B785F1E2B34E100ED9945 /* DKOrderDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 351B785D1E2B34E100ED9945 /* DKOrderDetailViewController.m */; };
351B78601E2B34E100ED9945 /* DKOrderDetailViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 351B785E1E2B34E100ED9945 /* DKOrderDetailViewController.xib */; };
3543B7751E2C765600F8F65C /* DKOrderSuccessViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3543B7731E2C765600F8F65C /* DKOrderSuccessViewController.m */; };
2
3
4
项目越大,文件越多,这里记录的信息就越多。我们现在的这个文件已经有三千多行了,我只选取了前面三行。 可以看到每一行的开头大概都是一个24位的16进制数字,然后就是一个文件名 ,在最后有一个 isa 和一个 fileRef(文件引用)。当然这里面的数据还包括很多东西,比如 Target、Group、FileRef 等就不再一一列举出来了。
# 为什么会冲突
上面简单说了冲突的罪魁祸首是 project.pbxproj,并简单说了每行的内容。很明显每一个文件都会有一个文件引用 (fileRef)。当多人协作的时候,在同一份项目中,不同开发者同时创建不同的文件,会出现创建出来的引用 fileRef 是相同的,但却指向了不同的文件。比如开发者A 创建了一个 ViewController,可能会产生这样一条记录 A:
D7921FD21E4B9ED1001C43E9 /* DKViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D7921FD11E4B9ED1001C43E9 /* DKViewController.m */; };
而 开发者B 创建了一个 xib文件,可能会产生这样一条记录 B:
D7921FD41E4B9F3F001C43E9 /* View.xib in Resources */ = {isa = PBXBuildFile; fileRef = D7921FD31E4B9F3F001C43E9 /* View.xib */; };
观察两条记录,就可以发现,相同的文件引用,指向了不同的文件。两个用户修改了同一个文件的同一块区域,git 会报内容冲突。坑爹的问题出现了,这是两个不同的文件,但是引用是同一个,无法像前言说的直接手动>>>找到冲突的位置,然后把两个都保留下来。因为一个引用不可能引用两个不同文件。这个冲突怎么办?只能先删掉一条引用记录,解决冲突后,再手动把被删掉引用的文件重新拉进项目。
# 优雅地解决
有没有办法解决这个冲突问题?答案是肯定的。在知乎上面搜了一圈,普遍有两种做法。一个是约定型的,另一个是解决型的。
# 约定型
需要增加文件时先增加完空文件后立刻 checkin 一次,让别人每次改动 pbxproj 的时候改动之前 checkout 一次,保证有交叉时间是可能性最小
就是你先创建一个空文件,生成了一个引用后,就push一遍代码。然后让别人pull,让他在创建新文件之前就已经有了你创建的空文件和生成的引用,这样相当于在你之后创建新文件,而不是同时创建了。
# 解决型
xUnique
开源 GitHub 地址
What it does & How it works:
- convert
project.pbxproj
to JSON format - Iterate all
objects
in JSON and give every UUID an absolute path, and create a new UUID using MD5 hex digest of the path- All elements in this json object is actually connected as a tree
- We give a path attribute to every node of the tree using its unique attribute; this path is the absolute path to the root node,
- Apply MD5 hex digest to the path for the node
- Replace all old UUIDs with the MD5 hex digest and also remove unused UUIDs that are not in the current node tree and UUIDs in wrong format
- Sort the project file inlcuding
children
,files
,PBXFileReference
andPBXBuildFile
list and remove all duplicated entries in these lists- see
sort_pbxproj
method in xUnique.py if you want to know the implementation; - It's ported from my modified sort-Xcode-project-file, with some differences in ordering
PBXFileReference
andPBXBuildFile
- see
大概的意思是:
- 替换所有 UUID 为项目内永久不变的 MD5 digest
- 删除所有多余的节点(一般是合并的时候疏忽导致的)
- 用 Python 重写了修改过的的 sort-Xcode-project-file 的排序功能,修改版相较原版增加了以下功能:
- 对 PBXFileReference 和 PBXBuildFile 区块的排序
- 使用脚本后如果内容没有变化不会生成新文件,避免一次不必要的 commit
# 安装方法
- 下载 xUnique,解压后, python 安装命令行脚本 setup.py
$ python setup.py install
- 验证是否安装成功
$ xunique -h
# 使用方法
使用方法有两种,一种是 Git hook,另一种是 Xcode "build post-action"。这里只用推荐的 Git hook 的方式。
- 创建 pre-commit 的钩子
$ { echo '#!/bin/sh'; echo 'xunique path/to/MyProject.xcodeproj'; } > .git/hooks/pre-commit
如果有使用 CocoaPods,还需要把 Pods.xcodeproj 也加上
$ { echo '#!/bin/sh'; echo 'xunique path/to/MyProject.xcodeproj'; echo 'xunique path/to/Pods.xcodeproj'; } > .git/hooks/pre-commit
- 给钩子加权限
$ chmod 755 .git/hooks/pre-commit
- commit
$ git commit -a -m "test xUnique"
Uniquify and Sort
Uniquify done
Sort done
Following lines were deleted because of duplication:
043592DF7047127330AF6F032495AC6C /* iflyMSC.framework */,
043592DF7047127330AF6F032495AC6C /* iflyMSC.framework */,
Uniquify and Sort done
File 'project.pbxproj' was modified, please add it and commit again to submit xUnique result.
NOTICE: If you want to submit xUnique result combined with original commit, use option '-c' in command.
Uniquify and Sort
Uniquify done
Sort done
Uniquify and Sort done
File 'project.pbxproj' was modified, please add it and commit again to submit xUnique result.
NOTICE: If you want to submit xUnique result combined with original commit, use option '-c' in command.
[master d9569e5] test xUnique
1 file changed, 566 insertions(+)
create mode 100755 xUnique.py
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
根据提示,两个 'project.pbxproj' 文件被修改了,需要 add,然后再次 commit。
看一下状态,果然被修改了。
$ git status -s
M Pods/Pods.xcodeproj/project.pbxproj
M SF.xcodeproj/project.pbxproj
2
3
- 再次 commit
$ git add .
$ git status -s
M Pods/Pods.xcodeproj/project.pbxproj
M SF.xcodeproj/project.pbxproj
$ git commit -a -m "test xUnique"
Ignore uniquify, no changes made to "/Users/bingo/Desktop/iOS/iOS Project/SF/SF.xcodeproj/project.pbxproj
Ignore sort, no changes made to "/Users/bingo/Desktop/iOS/iOS Project/SF/SF.xcodeproj/project.pbxproj
Uniquify and Sort done
Uniquify and Sort
Ignore uniquify, no changes made to "/Users/bingo/Desktop/iOS/iOS Project/SF/Pods/Pods.xcodeproj/project.pbxproj
Ignore sort, no changes made to "/Users/bingo/Desktop/iOS/iOS Project/SF/Pods/Pods.xcodeproj/project.pbxproj
Uniquify and Sort done
[master 861b932] test xUnique
2 files changed, 6251 insertions(+), 6253 deletions(-)
rewrite SF.xcodeproj/project.pbxproj (79%)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
根据提示,这个时候已经忽略排序了,因为之前已经排过一次序了,顺序没有改变。再看一下当前的 status,已经没有修改那两个文件了。
- 合并 commit
如果有改动了任意一个 project.pbxproj 文件,就需要 commit 两次。最好合并一下 commit,参考《Git 合并Commit》,比如用 rebase 来合并。
# 补充
如果用 Git 客户端如 GitHub Desktop 进行 commit 操作的时候,可能会报错找不到 xunique 命令,这时可以到 pre-commit 这个钩子中将 xunique 补全为绝对路径。
/usr/local/bin/xunique /path/to/project.xcodeproj
/usr/local/bin/xunique /path/to/Pods.xcodeproj
2
然后就可以直接在客户端跟往常一样进行 commit 操作了。在终端命令行操作则不会出现这个问题。
# 后话
以后 project.pbxproj 的冲突问题就解决了。但要注意,多人协作的时候,如果团队中的其中一个人使用了 xUnique,所有人就都要用 xUnique,否则 project.pbxproj 就会全乱掉。