编辑推荐: |
本文主要介绍了腾讯程序员的Git大法,是如何搞定分支的相关内容。希望对你的学习有帮助。
本文来自于微信公众号腾讯云开发者,由火龙果软件Linda编辑、推荐。 |
|
导读
很多读者看了《从9G到0.3G,腾讯会议对他们的git库做了什么?》之后,希望鹅厂程序员们分享更多
git 操作技巧。”git坑太多了“、”在工作中我经常遇到这个情况:忙了一天准备提交代码下班,结果
git 合并冲突把刚写好的代码覆盖掉了,血压飙升!““合并前文件还在的,合并后就不见了”,“我遇到
git 合并的 bug 了” ——这是程序员高频遇到的场景。鹅厂毕鸣一如何攻破这个 git 使用时的痛点?欢迎继续阅读。
01 背景
在一个岁月静好的一天,作为开发的你来到工位,看了看项目计划和待办事项,你发现,需要按顺序完成两个需求:
需求一:产品列表需求的开发。
需求二:用户管理需求的开发。
其中用户管理需求包括两个部分,即用户配置管理子需求和用户权限管理子需求。
根据前期会议对齐的结论,产品列表需求要求独立上线,产品管理的两个子需求要求一起上线。
于是,你分别从主干拉取了两个分支,一个是 feature/product_list,用来做产品列表需求的开发,一个是
feature/user_manager,用来做用户管理两个子需求的开发。
然后,岁月静好,你用了两周时间在 feature/product_list 分支开发完毕了产品列表需求的开发工作,进行提测。
然后切分支到 feature/user_manager 转而进行用户管理需求的开发工作,这个开发工作大概用时一个月,两个子需求各两周的开发周期。
又过了两周,岁月依然静好,你基本开发完用户配置管理子需求,
又过了一周,当你对用户权限管理子需求的开发进行到50%时,
项目节奏突然变了!
经过紧急开会对齐,你得到了一个消息,需求的优先级和上线时间进行了调整,为了能够满足客户要求,产品列表功能需要和用户配置管理子功能后天就要上线,为了提高效率,测试同学将一起测试这两个功能,测试通过后,再合入主干进行冒烟测试,之前的提测不再生效。
至于,用户权限管理子需求的交付时间,依然需要按时完成。
这时,然后你看着眼前的这两个分支,陷入了沉思。
这时,负能量爆棚的你先后尝试了以下几种方案:
方案一:讲道理
我:“跟项目组表示这两个子需求都在一个分支上,无法分开,且代码有关联,所以得等用户权限管理子需求开发完毕后才能提测。”
项目组的商务同学:“已经跟客户承诺,必须XXX前上线,不能等!”
方案二:心一横
我:“加个班把用户权限管理子需求做完,然后一起上线。”
项目组的测试同学:“十分认同你的工作态度,并表示自己不想加班多写一堆测试用例,也不想多测功能!”
家属同学:“你要是再晚回来就不让你进门了!”
方案三:心又一横
我:“跟项目组直接摆烂,表示只开发完了产品列表功能,用户配置管理子功能需要时间开发。”
项目组的项目管理同学:“进度我天天都在跟,你明明晨会上说用户配置管理子功能做完了!”
方案四:心再次一横
我:“决定下次再也不把两个子需求放一个分支了,再信XXX的话我就是狗,并表示一定要解决这个问题,并捍卫工程师“一定能解决工程问题”的尊严。”
然后,你又重新看了下 feature/user_manager 分支的代码,你发现,事情似乎没有这么糟,用户配置管理子功能的代码和正在开发的用户权限管理子需求的代码并没有那么的耦合,你可以通过文件目录来进行简单的区分。
这时,你想到了,可以发起两次向主干的合入,一次是将 feature/product_list 分支合入
master,一次是将 feature/user_manager 的部分目录合入 master。
项目组的测试同学提出了不同意见,他表示,他主要做代码合并前的功能测试,分两次发起合并,除了要做两次功能测试外,还可能会导致两个功能的联动逻辑测试不充分,把问题带到主干,测试同学希望的是,只发起一次合并,这样测试比较完整,问题比较可控:
你想了想似乎很有道理,但似乎又没有道理,这里到底应该选择哪一种其实也是一个有意思的点。
但这其实不是这篇文章的重点,因为不论是哪种方案,都会遇到一个相同的问题:如何将一个分支部分文件/文件夹优雅地合并到另一个分支。
OK,看起来这个问题的解决与否成为你是否成功捍卫工程师尊严的关键环节,那么我们来一起解决它。下面就是捍卫尊严的解决方案:
02 强行合并的方式
事实上 git checkout 是一个功能丰富的命令,比如最常用的切换分支:
还可以与 git branch 联合使用:
git branch A git checkout A
|
当然也可以用快捷方式:
同时 git checkout 后面除了跟分支,还可以跟某次提交和文件,这里就涉及到另一个功能:
恢复 WorkSpace 文件
git checkout [<commit>] [--] <paths>
|
即:用于拿暂存区的文件覆盖工作区的文件,或者用指定提交中的文件覆盖暂存区和工作区中对应的文件。
<commit> 是可选项,如果省略则相当于从暂存区(index)检出。这和
git reset 重置命令(例如 git reset HEAD <file>)大不相同:重置的默认值是
HEAD,而检出的默认值是暂存区。因此重置一般用于重置暂存区(除非使用--hard参数,否则不重置工作区),而检出命令主要是覆盖工作区(如果<commit>不省略,也会替换暂存区中相应的文件)。
该命令(包含了路径 <paths> 的用法)不会改变 HEAD 头指针,主要是用于拿指定版本的文件覆盖工作区中对应的文件。如果省略<commit>,则会拿暂存区的文件覆盖工作区的文件,否则用指定提交中的文件覆盖暂存区和工作区中对应的文
|
举个例子:
如果要放弃修改工作空间内容:
在git add命令执行前可以使用git checkout -- add.txt
在git add命令执行后可以使用git checkout HEAD -- add.txt
|
当然这两个命令不可逆,所以要慎重操作。
假设我们按照测试同学推荐的方案,即把 feature/user_manager 分支的部分目录合并到
feature/product_list 分支上 ,且需要合并的目录结构为/src/product/
步骤如下:
git checkout feature/product_list
git checkout feature/user_manager /src/product/*
|
意味着将 feature/user_manager 分支的 src/product 文件夹的内容强行覆盖到
feature/product_list 分支,但这个方法比较暴力,不推荐使用,原因有三个:
整个目录覆盖将作为一个完整的提交合并过来,不利于提交信息的追溯。
如果只有新增文件或者 src/product 文件夹下只有
feature/user_manager 分支进行修改,feature/product_list
没有修改,则没问题,如果两边都修改了,则存在代码和并和代码冲突的问题,这里并不能解决。
feature/user_manager 删除文件操作并不会同步过来,比如你在
feature/user_manager 分支删除了 src/product/test.xx
文件,但 feature/product_list 分支保留了 src/product/test.xx,这个时候
git checkout feature/user_manager /src/product/*并不会删除
feature/product_list 分支的 src/product/test.xx
文件(对,是的,不要怀疑)
|
03智能合并的方式
既然强制合并太暴力,那怎么智能合并呢?这里 git 没有直接的命令进行使用,需要一些工作技巧:
使用 git merge 命令
事实上 git merge 与 git rebase 是项目中经常使用的命令,有的时候会混淆了两个命令的概念,这里做一下简单的区分。
git merge 即就是常规的合并:
git merge feature //将分支 feature
合并到当前分支上
|
git rebase 即就是物理意义上的变基:
git checkout feature //切换当前分支为featrue分支
git rebase master // 将当前分支变基到当前分支(即feature分支)
|
两者的区别如下图所示:
主要的结论是:
git merge 就是真实意义上的合并,把两个分支的指针指向一起,同时将历史修改按时间顺序进行排布。
git rebase 就是分支变基,把合并进来的修改记录放在当前分支修改的前面。(时间上的前面)
git rebase 因为没有两个交叉修改记录看来很清爽,方便
CR。
git merge 因为保留的完整的修改记录,适合往联合开发环境下的主干或者主分支进行合并。(换句话说,合并到
master,一般使用的 merge)
当然实际项目中,一般在合并回 master 前,待合并分支先做
rebase,然后解决冲突,代码 CR,再合并,这样合并的时候就不会出现代码冲突,即可以自动化流水线完成。
|
在 feature/product_list 分支的基础上先创建一个新的分支 feature/product_list_temp。
git checkout feature/product_list
git checkout -b feature/product_list_temp
|
然后合并 feature/user_manager 分支到 feature/product_list_temp。
git
merge feature/user_manager --on-off |
将 feature/user_manager 分支合并到 feature/product_list_temp
后,这里通过 merge,将 src/product 文件夹下的代码进行合并,并解决了冲突,这时 src/product
的文件夹的代码被智能合并了,代码冲突解决了,同时保留了合并的历史记录。
再用强制合并方式中的 git checkout 命令强制把 product_list_temp 分支的
src/product 文件夹合并到 product_list 分支。
git checkout feature/product_list
git checkout product_list_temp
src/produc
|
这里解决了强制合并方式的问题2。
至于问题1,保留 product_list_temp 分支吧,嗯,虽然不太优雅,但在大的需求修改下,没有人力做细致合并的话,这样也是一个工程上有效的办法。
04
取巧合并的方式
智能合并的方式基本解决了强制合并方式的问题2,但也留下了问题1的坑,那有没有优雅的方法呢?
这里就要具体问题具体分析,首先,如果在 feature/user_manager 分支严格按照需求的顺序进行开发,那在用户配置管理子功能开发完毕的这个
commit_id,其实可以通过 git checkout 命令恢复回来,然后新拉个分支的方式合并回
feature/product_list 的方式解决。
在 feature/user_manager 分支上通过 checkout commmit_id
在本地会滚到那在用户配置管理子功能开发完毕的节点。
git checkout feature/user_manager
git checkout commmit_id
|
然后基于 feature/user_manager 分支的这个节点新建分支 feature/user_manager_temp。
git
checkout -b feature/tmp_user_manager |
将 feature/user_manager_temp 分支合并到 feature/product_list,这里通过
merge。
git checkout feature/product_list
git merge -b feature/tmp_user_manager
|
在 feature/product_list 分支合并到 master,这里通过 merge。
git checkout master
git merge -b feature/product_list
|
当然,如果在 feature/user_manager 分支交叉顺序对两个子需求进行开发,但每次提交都能是独立为某一个子需求开发提交出来,其实可以通过
git chery-pick 来解决。
智能合并中讲了 git mergr 和 git rebase 两个合并命令的区别,其实还有一种合并命令——gir
chery-pick。
使用 git chery-pick 命令
git chery-pick 相对于上面两个合并分支的命令,git chery-pick 主要是将某次/某几次提交进行合并。
git cherry-pick 的使用场景就是将一个分支中的部分的提交合并到其他分支,使用以下命令以后,这个提交将会处在
master 的最前面。
git checkout master
git cherry-pick <hashA>
<hashB>
|
如果 feature/user_manager 分支对 src/product
文件夹的修改主要来自于某次或某几次的提交(比如主要是完成某个需求或修改某个缺陷导致的修改),则可以直接使用。
git checkout feature/product_list
git checkpick <hashA>
<hashB> ...
|
这样就解决了强制合并方式的3个问题,因为本质上来讲,这次合并就是将 feature/user_manager
分支上几次提交,提交到 feature/product_list 上来。
05
优雅合并的方式
当然,取巧合并是预设前提的,如果对 src/product 文件夹的修改并不独立,比如,在 feature/user_manager
分支中某次提交中同时顺道为了用户权限管理子需求修改 src/product 和 src/config
两个文件夹怎么办?
一般来说,当你去问组内项目经验丰富的工程师时,大概率他会建议你用智能合并的方式。
如果你在纠结,这样就没有整个文件夹的修改记录了,项目经验丰富的工程师会建议在这次合并的 commit
上写上“欲看记录,去 product_list_temp 分支”看,并强调不要删除该分支。
如果你说,我不想这个方案,我就是想在当前分支看到所有修改,并优雅地合并某个文件夹的内容。
这个时候,绝大部分项目经验丰富的工程师会对你执着的精神表示认同,并不想再理你了。
但,既然看到这里了,笔者一定会一个兜底方案。
git checkout --patch source_branch
src/product
|
优雅的代价就是花一定的前置时间打基础。
git checkout -p 类似已交互的形式打补丁。
git 会跟你逐个掰扯 source_branch 分支上的 src/product 文件夹下的这些文件怎么处理。
是的,只要你愿意一个一个文件掰扯,你就能得到一个有完整提交记录的文件夹。
这时,你可能会有一个疑问,那和我一个一个修改文件有什么区别?
区别就是这样同时保留了代码提交的修改记录!
所以当你花了1个小时去逐个对齐了这个文件夹下每个文件的修改点后,你就可以跟测试说:提测! |