说说我以前不知道的git

 april 03, 2020     7 mins read

之前和同事因为git产生过不少矛盾,每次我都有一种“你的做法好像和我所听说过其他人的做法不太一样可是我又说不上来哪里不对”的感觉。为了能够长久的在技术讨论时立于不败之地(并不是),我在承诺给组里发表一个report以后开始带薪研究git的最佳流程。

这个研究做的其实比想象中时间更久,可能我真的不适合做research(还好没去学术界啊阿弥陀佛),研究清楚我具体想要解决哪些问题就想了很久。最终我简单粗暴地把研究课题定为How To Avoid Merge Conflicts并开始在Google中进行了graph search,发现网上能找到的现成资料的解释都非常浮于表面(比如怎么使用feature branch之类,还有要尽量多commit写清楚commit message等等,基本上在企业里用过几年git的都心里有数吧?)。再搜,发现其实我想要的信息全都在官方指南Pro Git Book里面,只是字太多了导致我一开始漏看了很多内容😂

这几天左右无事,准备把还记得的部分赶快用中文自己记录一下,算是给自己和需要的小伙伴们再科普一次,也是我开博以来第二篇技术干货啦啦啦~想跳过过程/原理等等直接看结论的,请直接跳到最后一个section并且欢迎来怼,毕竟都是一家之言😂

Commit, Tree & Blob

讲真,在我用git命令行的很长一段时间以来,根本不知道tree和blob是什么,对于commit的了解也仅限于“保留了这个项目的某个版本”。直到我开始在天少的推荐下用起了git客户端才发现居然还有tree/blob之类的东西。比如说如果你用git ls-tree [commit-sha]这个命令,你会看到类似以下的东西……

$ git ls-tree 0ab925e1f445e535fe5b0a936b0facc867fcf149
100644 blob 660170cc0ca2fd12955558577479d6599424f7e9    .gitignore
100644 blob f6d0daa4437cc3cc01f7b29fa2848d0fe9e4871f    CONTRIBUTING.md
040000 tree 1e3c518fe4933da3dfbfc37819d310552fe2b0eb    Dionysus.xcodeproj
040000 tree 37b1877603d7854991120cd677ea09efab33a393    Dionysus
040000 tree f83fd13949bf60d50db0a00f3259680f6d83c304    DionysusTests
040000 tree d5d5bdffe207a6a3c5a07ce376f1d5295959e254    DionysusUITests
100644 blob 9b3ac0e3f3c79ef5036bf9df37ea1da59289ff1a    Podfile
100644 blob 8dca0ef160daa18316a9e51da5d5da8dbddd71bc    README.md

同样的命令也可以用在tree/blob sha上面,比如说我用上面结果第四行那一大串作为输入,调用同一个命令

$ git ls-tree 37b1877603d7854991120cd677ea09efab33a393
100644 blob 7aad2e58f45aa609b78876aa30d3d759c44ee064    APIKeys_sample.swift
100644 blob 7fa1df09658fa0b3444785131f7c3389bf2f99b0    AppDelegate.swift
040000 tree a81076cc287adc42b745453dde5ce18299d395a9    Assets.xcassets
040000 tree f3c26079372eb516f03fad967cbe8e0eab5bc5d7    Base.lproj
100644 blob f4912c947b75992aaf1e8e486956bc25bc935ee1    Info.plist
040000 tree d4373b5b8be66ef42fe9f750d26bb834b51b93af    Preview Content
100644 blob e5544eedf22df7c9c82603211b56d2faa306d67c    RootView.swift
100644 blob f12e5d831957b545fbc473356c4a5575368b1385    SceneDelegate.swift
040000 tree 590fbf29e542792b2cf89eb2d9e055cb1fb795c8    core
040000 tree ed1593c5cb4ba63e36cb7cddaa5f0eecf8774f1d    views

所以这些都是什么东西呢?

blob比较好理解,就是某个文件的某个版本。比如说我一个repository有helloworld.txt,后来某次提交了对这个文件的改动。那么在这次提交之前的commits就会指向helloworld-blob-1,之后的就会指向helloworld-blob-2,直到再改为止。

tree根据官方说法就是一个类似Unix file system的存在,代表某文件夹的某版本。比如说上面这一段输出的意思就是在目前版本的Dionysus文件夹中,有7aad2e...版本的APIKeys_sample.swift等等文件,同时还有几个文件夹比如说a81076...版本的Assets. 话说我这之前一直没怎么搞懂这是啥,写这段文字的时候突然秒懂,参考名侦探柯南后脑勺划过一道闪电……

话说每个commit也是指向一个tree的,就是repository根目录当前版本的tree. 所以说每个commit实际上根本不像我之前以为的是一大帮code diff,而是……

The pointer to the top-level tree for the snapshot of the project at that point.

是整个项目的快照啊啊啊啊……我大概是因为平时rebase用的太多才会天真的以为每个commit是一堆code diff吧!

Conflicts是如何产生的

一般情况下把两个错开的分支捏一块儿无非两招,merge & rebase.(之所以强调“错开的”分支是因为我不想讨论无关主题的fast forward merging)

为了解释原理我们先假设这么一个场景——某个文件里面有一行"hello world"

  1. 爱丽丝小姐姐把hello改成了大写
  2. 鲍勃小哥哥先把world删了,commit,再把world加了回来,commit,最后加了个感叹号,commit.

Three-Way Merge

基本上就是把两个commit的快照对比一下,哪行出现了多个版本,就解决哪行的conflict,解决完后产生新的blob/tree并以此建立一个新的merge commit。

比如要merge上面俩小伙伴的代码会出现以下情况——merge conflict between "Hello world" and "hello world!",所以这行到底应该是什么?于是我把这行改成"Hello world!"提交即可。

结论:你只用解决一次conflict。

Rebase

Rebase比merge烦太多了,因此让业界很多小伙伴感到头疼。Rebase的工作原理就是——真的提炼出每个commit的code diff,然后按顺序把这些code diff在另一个分支上重复一遍。

比如上面情况,假设鲍勃小哥想要rebase onto爱丽丝小姐姐的分支,那么在他输入命令回车的瞬间git会问他——她的版本是"Hello world"你的版本是"hello",请问你选哪个?

  • 如果选鲍勃版本"hello",那么爱丽丝那个commit大概要作废了。
  • 如果选爱丽丝版本"Hello world",那么等作用下一个commit的时候git又会问一遍,“Hello world”和“hello world”你选哪个?如果选了前者的话,可能后面还要再问一遍"Hello world"和“hello world!”你选哪个……
  • 如果直接在这里改成"Hello"那么和选"Hello world"一样,接下来每次作用新的commit,他就要修一次merge conflict,实惨

结论:你要解决O(n)次conflict。

当然目前业界明显是偏爱rebase多于merge的,理由显而易见,毕竟事后出了问题debug起来不要比merge容易更多啊~想象一下我发现某一行有bug,怒输入git blame回车,结果一看特么的"Merge branch feature-XXX..."而且当你排查feature-XXX的时候很可能bug消失了,你说气不气?而且git还有个我没用过的神器叫git bisect,二分查找debug的,那你要能二分查找岂不是得保证commit log得是线性的嘛。

Squash

因此我还研究了一下什么时候应该用squash。比如鲍勃小哥哥如果在rebase之前把他的三个commit都squash了,那就好办了,rebase的时候解决一次conflict即可。

那问题来了,万一他提交完第一个第二个commit以后,我从他的分支又起了个分支并且提交了n个commit。当我想重新把自己的分支merge回他的分支的时候(注意是merge,不是rebase),发现他竟然squash了……然后问题就来了,我的branch里面还有他rebase之前的commits呀,直接merge的话会出现一个“同一版代码在git history中以不同的sha出现两次”的诡异情况……

如何解决conflicts

我是这么想的——一切取决于你的分支上还有没有其他分支。

  • 没有的话,放心squash and rebase,一次性解决全部conflicts,还能灭掉很多"fix typo"之类的鸡肋commit message.
  • 有而且其他分支上有比较多比较大的改动的话,直接用merge,不要再重新生成一堆新的commits。毕竟这个分支本来就是作为一个,嗯—— 母节点(?)而不是子节点而存在的,以后看git log的时候能够诚实反应各种dependencies不是也挺好的嘛。

引用Pro Git Book上一句箴言——

Do not rebase commits that exist outside your repository and that people may have based work on.

然而我现在辞职了,大概也不会想事后诸葛亮地回去battle前同事了。不知道以后能不能用来怼新同事……

disclaimer: ginsterrific.com and the author give no guarantee and accept no responsibility or liability of the accuracy or the completeness of the materials and information contained. opinions are my own and do not necessarily reflect the views of my employer or any of my affiliations.