« Unity3D 的大场景内存优化 | 返回首页 | MMORPG 客户端的网络消息框架 »

为什么美术和策划在使用 git 时会遇到更多麻烦

我们公司有两个项目的客户端在使用 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 中相同的文件;

  1. 那么如果三个版本完全一致,这个文件就保持原样就够了,因为 A 和 B 都没有修改过。
  2. 如果 A 和 B 中的文件版本都和 C 不一致,但是 A 和 B 的版本之间又是相同的,那么它们的修改是一样的,就取改过的就好了。
  3. 如果只有 A 和 C 不一致,或只有 B 和 C 不一致;那么就认为只有一方修改,那么取修改的一方即可。
  4. 冲突发生在这里,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 被分别合并到了两个分支上,这个合并过程,甲和乙做起来都不难,顺手就搞定了。但是在最后甲和乙做大合并时,埋下的炸弹才爆炸。

如果在开发过程中,不断的这种随意合并,一开始冲突比较好解决;但是积累多了以后,就变成了选择难题。

程序员比较少碰到这种选择难题,我想是因为:

  1. 程序员即使是对待开发路径上比较顺利的 merge 过程,也会比较谨慎的审核,心里对变更跟有底。而文本合并更容易看到差异全貌,也方便了做这种审核。所以问题比较少累积。

  2. 即使是并非自己经手的数据文件,最终的合并行为也可以单独的被正确处理,而不是只有使用我们的,使用他们的一条路。

  3. 文本文件可以被拆分的更细,往往系统就把差异处理好了,不必抛给人来审查。

Comments

美术用Adobe Version Cue CC、策划用Sharepoint、程序用Git,就这么定了,于是再也没出过什么问题 =。=
感觉你举的例子不大可能出现 实际中是有1个主分支master(应该就是A1B1C0吧)的,甲和乙合并的情况是不可能发生的,而甲和乙也只会把master分支merge到本地 A0B3C2会先以MergeRequest的形式合并到master 比如乙比甲先提交 乙会从master分支中将A0B3C2合并到A1B1C1得到A1B5C3(乙手动处理了冲突)然后再次以MergeRequest的形式合并到master 甲在合并时会从master拉取,包含了乙的结果(乙手动处理了冲突)
感觉你举的例子不大可能出现 实际中是有1个主分支master的 A0B3C2一般都会以MergeRequest的形式合并到origin/master(应该就是A1B1C0吧) 而甲和乙也只会把origin/master分支merge到本地
美工设计的图像等二进制数据也确实没什么好办法来管理,git,svn都很勉强,diff都看不出来,图像也没法merge
想象一下git禁用了差异比对和merge功能的效果吧。
有没有考虑过使用perforce呢? perforce stream功能强大迅速,可以对于数据二进制文件进行lock进而无法checkout,避免无法merge的情况,而对source文件不进行lock。最适合游戏开发的一点在于它修改文件时checkout,提交或者查看时不用去扫描改动。像svn在windows目录下文件太多时,每次扫描改动会变得非常慢,不太熟悉git,不知道git是否有提升。
从旧版本中去bugfix的可能性不高吧,一般都是合并后的新版本中去bugfix,也可能只是举例子。 其实是个三路合并变四路合并的问题,也是要新建一个A0B0C0的D分支,让D去合并A,B,C这样就可以保证每次合并只有一个根节点。
藏拙
的确是破了影响也不会太大:1、破的代价太大,对二进制文件改动收益不多,2、即使破了也容易换一种更难破的算法。但使用git只需缓存hash文件对分布式开发带来的收益更可观,而且统一的管理对协作和管理的效率也是乐观的
说 SHA-1 被破了的人一般不明白什么叫被破了,到底有什么影响。
文本文件容易使用SHA-1之类的解决识别问题,二进制文件用起来可能:1、计算hash时间长,2、容易有冲突。网上已经报道SHA-1被破了
我们是用P4V
文件分类上,项目的资源文件版本和代码的版本是由关联的,所以和代码做到一个版本管理里是有必要的。其他设计文档和代码关联性不大,可以独立使用文档管理工具管理。 操作方式上,给美工装个TortoiseGit,把git当svn用,傻瓜式操作。 版本冲突问题只能靠规范和沟通了,不管用什么工具都是会存在的问题。
有人已经总结的很好了,项目基本就是代码,用git比较合适 ,如果项目里除了程序员的代码,还有策划的设计文档,美术人员的图档资源,用svn比较好。
@glcolor 我们使用 git 的需求来源于项目需要同时维护超过 5 个以上不同的版本。而这些不同的版本差异不太适合用程序项目中常用的条件编译(u3d 项目的数据资源对条件编译支持不完善)的方式来平坦放在同一组文件中。 去中性化的分支需求,也是日常开发中的常见场景,例如部分人合作开发一个新特性。其实在基于 svn 的项目中,我们依然需要额外开发一套带三路合并机制的去中性化协作服务来辅助开发,解决:这块东西我需要和另外几个人共享,但我不需要提交到 svn 仓库这类问题。 svn 开分支太慢是痛点。 分支才是需求,分布式开发并不是需求。
关于分支合并的问题,我觉得GIT的设计并没有问题,问题在于,GIT不是这么用的;GIT的分支合并的正确用法是“新建分支->修改代码->提交修改->管理员视情况选择合并或者不合并”,这是一种简短的、清晰的、社区协作型的先进工作方式。 而“新建分支,在分支上做大量工作,然后多分支合并”,这种仍然是传统的工作流程(比如SVN就是这样的),并不符合GIT的设计初衷,所以会遇到你们讨论的那些问题。简而言之,不是GIT的问题,是使用方式的问题,你们的项目不适合使用GIT来管理。
GIT当初要解决的核心问题是分布式开发的问题,是针对以社区为单位进行分布式协作开发的大型开源项目的量身定制,GIT的很多特性,都是由此而被设计出来的,这些特性中的大部分,对于严密管理的中小型团体来说,并没有太多的助力,部分特性反而是有害的,所以我觉得,没必要强行推广GIT。
学习了
学习了
学习了
为什么不一个项目一个地址,美工只有推的过程,这样不就不存在冲突了?
要让策划和美术用git感觉这个难度很高,他们的思维模式就跟程序员不一样,历史经验得出,还不如弄个samba共享定时备份来得简单和稳定。
@elfmedy 首先我并不是实用就好理论,不称手的工具妨碍到工作了当然也是不行。就拿云风的理论来说吧,感觉C比 C++好。他自己用的UNITY3D是不是C++写的?UE4是不是C++写的?我是希望他能用C写一个超过它们的。lua 呢做为脚本使用是没问题的,但是云风喜欢什么就过度,重度使用,他发的碰到的性能问题,bug的帖子也 不是一个两个了。git or svn? 我感觉项目把版本搞的清楚,不是太乱的话,什么都可以吧?我一直用svn ,上线的游戏好几个了,没感觉有什么问题。我为什么要讲这些?因为我感觉他团队的人苦啊。要按他的想 法去学没必要掌握的东东。策划,美术学毛git?不是搞笑吗?你是程序员,还要求所有人都有程序员思维? 搞笑,还给人扣实用主义的帽子?git合并容易出问题,资源完全没必要用
@fly 推广了你不用就好了,你不也在这给人广播你的所谓实用就好理论?
版主总是喜欢很偏激的去推广一些东东,比如C编程了,lua了,git了。我每次看看也就笑笑,我就想你写了那么长时间程序,为什么热情总在这些东东上面了?我们也写10几年代码。不感觉这些东东对游戏开发有什么影响啊。游戏也大卖啊。版本也OK啊。哪怕早期的VSS也不感觉有多不好用。这些不是游戏开发的重点。
最后的例子写的很好, 这一点就是我对git印象最不好的地方. git越每个人都有自己搞自己一套版本管理的自由,越会给合并时埋下炸弹. 这方面svn提倡更敏捷的尽早提交尽早解决冲突,冲突问题对git来说也是早晚躲不掉的,越早解决反而越容易,而且遇到的冲突不会像这个例子里那样感觉困惑,因为冲突的地方也是自己修改过的,解决起来很明确.
二进制文件是不鼓励进入版本管理的,因为无法做diff,结果就变成了备份管理,发生冲突时就变成了覆盖和被覆盖。 而office系列有它自己的版本控制,请参照SharePoint,里面做的也很不错。
其实不一样的,明确显示出区别,即便不能自动合并,用户也会有更深刻的印象,不至于说根本都不知道那个文件改过了。如果只是有个文件列表,那么多文件呢,一般人不会去看的。但是美术对图形很敏感的,圈出不一样的地方,他会记得住,心里就有数了。当然这个也不是绝对的。
所以还是没明白最终这两个角色应该用什么规范来做,请问可以分享培训资料吗?
@Abner 通常这种情况都是做 rebase 简化关系。不过对于策划都用二进制数据文件,rebase 往往是个灾难。 因为 rebase 就是是把 commit 一个接一个的 merge 回去。而二进制文件无法自动 merge ,遇到的麻烦会比纯文本多的多。 @ephay 本质问题我在文中写了。就是文件的粒度还不够,纯文本可以被拆分为行来做三项合并就有更大的优势。 光圈出不同的像素是不够的,必须能做到自动合并。但自动合并对图片来说是非常困难的。
文中例子,如果是甲来处理合并,因为C并没有经过他的修改,常规的想法是直接采用乙的版本,咋会疑惑呢?
我们是这样子的:Git用来做代码版本控制,svn用来做文档,美术资源的管理。
我觉得问题的关键就在于,这类工具是专门设计给纯文本文件使用的,处理二进制文件,尤其是图片的时候就非常不友好。如果它能像处理文本一样,直接显示两张图,并圈出不同的像素,美术也会像程序员一样用的得心应手的。
真的是让我对Git的合并有了更深的了解。文中最后一例,正确的处理方法是不是在bugfixes后,甲和乙都做个rebase呢?

Post a comment

非这个主题相关的留言请到:留言本