我们公司有两个项目的客户端在使用 git 做项目管理,三个项目使用 svn 管理。程序员比较爱 git ,但是为什么 svn 还存在?主要是在做客户端开发时,策划和美术人员始终迈不那道坎。即使已经在用 git 的项目,策划们还是反应用起来比 svn 跟容易犯错误,遇到自己无法解决的问题也更多。
我是非常想在公司全部推广使用 git 做项目管理的。所以需要仔细考察到底是什么东西阻止了策划(及美术)们掌握这个工具。
思考一番后,我认为一套完整的培训机制还是得建立起来。完全靠自学和口口相传是不靠谱的。尤其是在有 svn 基础的时候,非程序开发人员接受 git 其实比程序开发人员要困难的多。如果你硬要把 svn 的概念全部适配到 git 上,其实就是在把 git 当 svn 在用,不仅获得不了好处,反而增加了很多困扰。
而实用主义者,在没有外力的情况下,只会看到表面。不可能系统的从原理上系统理解 git 到底解决了什么问题、每步操作背后到底做了什么,如果出现了问题,问题是怎么引起的。我们知道,在用 git 的时候,由于分支和提交都比 svn 方便,分布式的结构也会更容易导致版本演化图变得异常复杂。当它乱成一团乱麻的时候,任何新的合并操作都会比之前遇到更多麻烦。如果使用者心里有清晰的概念,时刻保持演化关系简单,他遇到的问题自然会少。而你遇到问题乱解决一通,只满足于把现在的问题搞定,那么下次就会面临更大的灾难。
过去在网易,我们建立了一套培训机制。入职开发人员(至少是所有校招人员),无论是做策划还是程序还是美术,都必须经过 svn 的培训。只有建立其版本管理的概念,日后工作中才会少犯错误。
而从基本概念上讲,git 其实并不比 svn 复杂多少,缺少的正是这套流程。让使用者知其然也知其所以然。
今天我自己做了一份材料,晚上尝试对部分同事做了两个小时的讲座。材料其实不重要,网上可以找到比我做的详细的多,好的多的教程。重要的是互动教学的部分。听的人可以讲出自己的疑惑,然后立刻得到解答。
我最想教授的就是 git 根本的原理,而操作这些反而是次要的。因为你不记得的时候总可以在网上搜索到。
首先,我觉得最关键、也是最容易被学习者遗漏的是,git 是有工作区、暂存区、历史记录(仓库)三个区域的。而 svn 只有工作区和中心服务器的仓库两个,没有暂存区。如果只记几个操作,完全不用知道这些,硬去背操作就好了。但是出了问题就容易懵。
我们平常工作的文件,都是在工作区修改。这和 svn 并没有什么不同,也不需要特别讲解。但是,一旦工作做到一个段落,需要提交到大家共享的仓库,情况就变得不同了。
暂存区实际可以勉强看成 svn (图形化界面)的那个提交面板上的多选框。svn 和它的前辈 cvs 最大的不同就是文件是做成一个集合同时提交的,提交是原子性的,要么一起成功,要么一起失败。而 cvs 是基于单个文件的。所以我们需要把提交声明成一个集合。
svn 采用的方法是在提交时勾选,而 git 采用的是设立一个中间容器,可以用 git add , git rm ,git mv 逐步把想修改的东西复制进去,然后在生成提交集的时候原子性搞定。由于提交总是针对本地仓库,就总是成功;而 svn 针对是远程仓库,有可能失败。
git 允许大家协作时共享的数据保存在不同的地方,只要数据严格相同,就认为是同一份数据。这个设计允许每个创作人员有最大的自主性,因为你可以在这份数据上任意修改(包括开设分支),只在最后要和别人同步的时候再同步。但是坏处就是,事实上每个人的那份共享数据又各不相同,这是因为大家的同步时机不可能相同。
我们要获得创作自主性这个好处的同时,就必须承担手动同步共享数据这个责任。也就是 fetch merge (pull) push 这些操作。这些操作在用 svn 时是不存在的,用 git 时,如果你不了解上面的细节,自然也容易忘记。
同样,reset checkout revert merge 等等都是在对三个区之间挪动数据的不同操作。如果不去理解操作指针、不去理解工作区和暂存区的关系,应去记 git reset --hard 可以回退版本,远程仓库的回退要 revert ,碰到冲突时就无法理解问题是怎么产生的。
而概念其实不那么理解,需要的只是认真去了解。
我个人认为, git 的本质就在于管理好版本演化的图。图的每个节点,也就是 commit 意味着一组对文件的修改。commit 和 commit 的关系就是谁在谁基础上做的修改,构成了节点间的连线。git 的复杂点在于,每个 commit 都可能基于多个 commit 生成的,让这些节点不再是一条直线连接。
这个复杂性还是为了解决更灵活的,从任何一个位置衍生创作这个需求。表现在 git 里就是无限制的开分支。
分支对协作开发非常的重要,因为开发人员总需要自己试验点什么而不需要和别人达成默契(不需要把修改推送到中央服务器);总有开发团体中的部分人间的协作,而不想影响到另一部分人;长期维护的项目总有各种不同的版本需要同时维护……
灵活的分支是 git 和 svn 根本性的不同,它也带来了开发流程上的革新,所谓支持分布式开发只是一点点副作用而已。
所以每个使用者也都有责任维护好整个项目的 commits 构成的图不要失控,乱成一团。
为什么策划和美术更容易遇到麻烦?
我想,所谓麻烦,都是和 merge 有关,也就是要重新连接图上面无关的两个(多个)节点时需要做的事情。merge 就是把图上两个节点用一个新节点连起来,然后后面的人就可以从一个统一点开始i继续演化。这里面就包括了把节点对应的数据内容合理的融合在一起,而不光是连个线。
融合的算法基础叫做三路合并。也就是,当你想让 A 和 B 合成一个时,版本管理器会去追寻 A 和 B 之前是在哪里分开的。大多数情况下可以找到一个公共的节点 C 。我们就可以认为 A 路线和 B 路线在走到 C 之前是完全一致的,我们不用理会,要做的是让 C 到 A 与 C 到 B 殊途同归。
由于每个修改集是有更零碎的针对单个文件的修改,我们检查 A 和 B 中,如果有和 C 中相同的文件;
- 那么如果三个版本完全一致,这个文件就保持原样就够了,因为 A 和 B 都没有修改过。
- 如果 A 和 B 中的文件版本都和 C 不一致,但是 A 和 B 的版本之间又是相同的,那么它们的修改是一样的,就取改过的就好了。
- 如果只有 A 和 C 不一致,或只有 B 和 C 不一致;那么就认为只有一方修改,那么取修改的一方即可。
- 冲突发生在这里,A B C 分别是完全不同的三个版本,这就要留给人来决策。
当文件是纯文本的时候,内容其实是按行分成更小的单位的。我们可以理解成是更细碎的小块。所以即使发生了 4 ,大部分情况下,分得更细的块之间依然可以自动处理好分歧。
但是策划一般用 excel word 这种二进制格式,美术更是以二进制图片模型数据为主。享受不到拆分更细的自动消除分歧过程,需要手工处理的东西更多。
不过通常,这也不是大问题。策划可以简单的用使用我的版本或使用他的版本,在 A B 中二择,帮助系统完成三路合并。
那么困扰策划们的问题出在哪里呢?我认为是 “交叉合并” 的情况。也就是需要合并的 A 和 B 并没有简单的一个分差点 C ,而是多个。git 在处理这种情况有多套方案,默认的是递归处理。把合并问题转换为一个个三路合并的子问题。在解决子问题的过程中,很可能就需要多次的寻求人的帮助确认。简单的单次用我的版本或用你的版本很容易犯错。
我一下找不到合适的实际例子,随便编一个,可能不太合理,姑且看看:
如果一开始的一处 commit 修改了 A B C ,我们把它的版本叫做 A0B0C0 。然后,版本推进到了下一个版本,修改了 AB 没有动 C ,版本变成了 A1B1C0 。
这个时候,两个人分别在 A1B1C0 的基础上做跟进,甲改了 B1 成 B2 ,乙改了 C0 成 C1 。我们看到的版本演化大致是这样的:
A0B0C0 ------- A1B1C0 +------- A1B2C0(甲)
+------- A1B1C1(乙)
这时候,在 A0B0C0 上发现了一个 bug ,有人修复了。修复涉及到 B 和 C 的修改,这样就产生了 B3 和 C2 。我们暂把 A0B3C2 这个版本成为 bugfix 分支。
A0B0C0 +------ A1B1C0 +------- A1B2C0(甲)
| +------- A1B1C1(乙)
+------ A0B3C2(bugfix)
这时,甲和乙都知道这个 bug 修改了,他们都把这个 bugfix 合到了自己的分支上。三路合并的结果是 A 保留了版本 A1 ,甲发现 B 被两者都改了,他在 B2 和 B3 的基础上创作了新的 B4 ;乙除了要在 B1 和 B3 的基础上创作出 B5,还发现发现 C 也都改了,就在 C1 和 C2 的基础上创作了 C3 。
现在版本图就成了这样:
A0B0C0 +------ A1B1C0 +------- A1B2C0(甲) ---------------A1B4C2
| +------- A1B1C1(乙)------A1B5C3 |
| / |
+------ A0B3C2(bugfix)-----------------/-------------+
甲和乙的版本,也就是 A1B4C2 和 A1B5C3 要合并的那一刻,问题就不再是简单的三路合并了。因为这两个版本有两个共同的根:A1B1C0 和 A0B3C2 。
如果是甲来处理合并,他会非常疑惑。因为再次之前,甲并没有修改过 C ,他手头上的 C2 是在之前和 bugfix 分支三路合并自动合过来的。他当时并没有在意。但是现在他要处理 A1B4C2 和 A1B5C3 两个版本汇总的 C2 (bugfix 带过来) 和 C3 的冲突了。而 C 文件似乎并没有经过他的修改。
在这个例子里,烦恼的核心在于 bugfix 被分别合并到了两个分支上,这个合并过程,甲和乙做起来都不难,顺手就搞定了。但是在最后甲和乙做大合并时,埋下的炸弹才爆炸。
如果在开发过程中,不断的这种随意合并,一开始冲突比较好解决;但是积累多了以后,就变成了选择难题。
程序员比较少碰到这种选择难题,我想是因为:
程序员即使是对待开发路径上比较顺利的 merge 过程,也会比较谨慎的审核,心里对变更跟有底。而文本合并更容易看到差异全貌,也方便了做这种审核。所以问题比较少累积。
即使是并非自己经手的数据文件,最终的合并行为也可以单独的被正确处理,而不是只有使用我们的,使用他们的一条路。
文本文件可以被拆分的更细,往往系统就把差异处理好了,不必抛给人来审查。