解决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。

1
2
3
4
/* 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 */; };

项目越大,文件越多,这里记录的信息就越多。我们现在的这个文件已经有三千多行了,我只选取了前面三行。 可以看到每一行的开头大概都是一个24位的16进制数字,然后就是一个文件名 ,在最后有一个 isa 和一个 fileRef(文件引用)。当然这里面的数据还包括很多东西,比如 Target、Group、FileRef 等就不再一一列举出来了。

为什么会冲突

上面简单说了冲突的罪魁祸首是 project.pbxproj,并简单说了每行的内容。很明显每一个文件都会有一个文件引用 (fileRef)。当多人协作的时候,在同一份项目中,不同开发者同时创建不同的文件,会出现创建出来的引用 fileRef 是相同的,但却指向了不同的文件。比如开发者A 创建了一个 ViewController,可能会产生这样一条记录 A:

1
D7921FD21E4B9ED1001C43E9 /* DKViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D7921FD11E4B9ED1001C43E9 /* DKViewController.m */; };

而 开发者B 创建了一个 xib文件,可能会产生这样一条记录 B:

1
D7921FD41E4B9F3F001C43E9 /* View.xib in Resources */ = {isa = PBXBuildFile; fileRef = D7921FD31E4B9F3F001C43E9 /* View.xib */; };

观察两条记录,就可以发现,相同的文件引用,指向了不同的文件。两个用户修改了同一个文件的同一块区域,git 会报内容冲突。坑爹的问题出现了,这是两个不同的文件,但是引用是同一个,无法像前言说的直接手动>>>找到冲突的位置,然后把两个都保留下来。因为一个引用不可能引用两个不同文件。这个冲突怎么办?只能先删掉一条引用记录,解决冲突后,再手动把被删掉引用的文件重新拉进项目。

优雅地解决

有没有办法解决这个冲突问题?答案是肯定的。在知乎上面搜了一圈,普遍有两种做法。一个是约定型的,另一个是解决型的。

约定型

需要增加文件时先增加完空文件后立刻 checkin 一次,让别人每次改动 pbxproj 的时候改动之前 checkout 一次,保证有交叉时间是可能性最小

就是你先创建一个空文件,生成了一个引用后,就push一遍代码。然后让别人pull,让他在创建新文件之前就已经有了你创建的空文件和生成的引用,这样相当于在你之后创建新文件,而不是同时创建了。

解决型

xUnique

开源 GitHub 地址

xUnique 下载地址

What it does & How it works:

  1. convert project.pbxproj to JSON format
  2. 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
  3. 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
  4. Sort the project file inlcuding children, files, PBXFileReference and PBXBuildFile 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 and PBXBuildFile

大概的意思是:

  • 替换所有 UUID 为项目内永久不变的 MD5 digest
  • 删除所有多余的节点(一般是合并的时候疏忽导致的)
  • 用 Python 重写了修改过的的 sort-Xcode-project-file 的排序功能,修改版相较原版增加了以下功能:
    • 对 PBXFileReference 和 PBXBuildFile 区块的排序
    • 使用脚本后如果内容没有变化不会生成新文件,避免一次不必要的 commit

安装方法

  • 下载 xUnique,解压后, python 安装命令行脚本 setup.py
1
$ python setup.py install
  • 验证是否安装成功
1
$ xunique -h

使用方法

使用方法有两种,一种是 Git hook,另一种是 Xcode “build post-action”。这里只用推荐的 Git hook 的方式。

  • 创建 pre-commit 的钩子
1
$ { echo '#!/bin/sh'; echo 'xunique path/to/MyProject.xcodeproj'; } > .git/hooks/pre-commit

如果有使用 CocoaPods,还需要把 Pods.xcodeproj 也加上

1
$ { echo '#!/bin/sh'; echo 'xunique path/to/MyProject.xcodeproj'; echo 'xunique path/to/Pods.xcodeproj'; } > .git/hooks/pre-commit
  • 给钩子加权限
1
$ chmod 755 .git/hooks/pre-commit
  • commit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ 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

根据提示,两个 ‘project.pbxproj’ 文件被修改了,需要 add,然后再次 commit。

看一下状态,果然被修改了。

1
2
3
$ git status -s
M Pods/Pods.xcodeproj/project.pbxproj
M SF.xcodeproj/project.pbxproj
  • 再次 commit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ 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%)

根据提示,这个时候已经忽略排序了,因为之前已经排过一次序了,顺序没有改变。再看一下当前的 status,已经没有修改那两个文件了。

  • 合并 commit

如果有改动了任意一个 project.pbxproj 文件,就需要 commit 两次。最好合并一下 commit,参考《Git 合并Commit》,比如用 rebase 来合并。

补充

如果用 Git 客户端如 GitHub Desktop 进行 commit 操作的时候,可能会报错找不到 xunique 命令,这时可以到 pre-commit 这个钩子中将 xunique 补全为绝对路径。

1
2
/usr/local/bin/xunique /path/to/project.xcodeproj
/usr/local/bin/xunique /path/to/Pods.xcodeproj

然后就可以直接在客户端跟往常一样进行 commit 操作了。在终端命令行操作则不会出现这个问题。

后话

以后 project.pbxproj 的冲突问题就解决了。但要注意,多人协作的时候,如果团队中的其中一个人使用了 xUnique,所有人就都要用 xUnique,否则 project.pbxproj 就会全乱掉。

  • 本文作者: Bingo
  • 本文链接: https://blog.bingo.ren/11.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!