首先,当你读到这篇文章的时候,可能已经进入到这个需求的场景了,但笔者还是想构建一个常见的业务场景,以希望读者能够更快的进入到这个问题背景中:
在一个岁月静好的一天,作为开发的你来到工位,看了看项目计划和待办事项,你发现,需要按顺序完成两个需求:
1.产品列表需求的开发,2.用户管理需求的开发
(其中用户管理需求包括两个部分,即用户配置管理子需求和用户权限管理子需求)
根据前期会议对齐的结论,产品列表需求要求独立上线,产品管理的两个子需求要求一起上线
于是,你分别从主干拉取了两个分支,一个是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,看起来这个问题的解决与否成为你是否成功捍卫工程师尊严的关键环节,那么我们来一起解决它。
===下面这里是对git checkout命令进行知识点的补充,想直接看方案的可以略过===
事实上 git checkout是一个功能丰富的命令,比如最常用的切换分支
git checkout A //切换到A分支
还可以与git branch联合使用
git branch A //新建A分支
git checkout A //切换到A分支
当然也可以用快捷方式:
git checkout -b A //新建A分支并切换到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 // HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和以及工作区中的文件
当然这两个命令不可逆,所以要慎重操作
===上面这里是对git checkout命令进行知识点的补充,想直接看方案的可以从这里继续看===
假设我们按照测试同学推荐的方案,即把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分支
但这个方法比较暴力,不推荐使用,原因有三个
1.整个目录覆盖将作为一个完整的提交合并过来,不利于提交信息的追溯
2.如果只有新增文件或者src/product文件夹下只有feature/user_manager分支进行修改,feature/product_list没有修改,则没问题,如果两边都修改了,则存在代码和并和代码冲突的问题,这里并不能解决
3.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文件(对,是的,不要怀疑)
既然强制合并太暴力,那怎么智能合并呢?这里git没有直接的命令进行使用,需要一些工作技巧:
===下面这里是对git merge和git rebase命令进行知识点的补充,想直接看方案的可以略过===
事实上git merge 与 git rebase是项目中经常使用的命令,有的时候会混淆了两个命令的概念,这里做一下简单的区分
git merge即就是常规的合并
git rebase即就是物理意义上的变基
两者的区别如下图所示
参考资料: https://juejin.cn/post/7034793065340796942
主要的结论是
git merge就是真实意义上的合并,把两个分支的指针指向一起,同时将历史修改按时间顺序进行排布
git rebase就是分支变基,把合并进来的修改记录放在当前分支修改的前面(时间上的前面)
git rebase 因为没有两个交叉修改记录看来很清爽,方便CR
git merge 因为保留的完整的修改记录,适合往联合开发环境下的主干或者主分支进行合并(换句话说,合并到master,一般使用的merge)
当然实际项目中,一般在合并回master前,待合并分支先做rebase,然后解决冲突,代码CR,再合并,这样合并的时候就不会出现代码冲突,即可以自动化流水线完成
===上面这里是对git merge和git rebase命令知识点补充,想直接看方案的可以从这里继续看===
在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/product
这里解决了强制合并方式的问题2
至于问题1,保留product_list_temp分支吧,嗯,虽然不太优雅,但在大的需求修改下,没有人力做细致合并的话,这样也是一个工程上有效的办法
参考资料: https://blog.csdn.net/weixin_43758377/article/details/123394291
智能合并的方式基本解决了强制合并方式的问题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>
参考资料:Merge,Rebase,Cherry-Pick 了解一下 - 知乎
===上面这里是对git chery-pick命令进行知识点的补充,想直接看方案的可以从这里继续看===
如果feature/user_manager分支对src/product文件夹的修改主要来自于某次或某几次的提交(比如主要是完成某个需求或修改某个缺陷导致的修改),则可以直接使用
git checkout feature/product_list
git checkpick <hashA> <hashB> ...
这样就解决了强制合并方式的3个问题,因为本质上来讲,这次合并就是将feature/user_manager分支上几次提交,提交到feature/product_list上来
当然,取巧合并是预设前提的,如果对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个小时去逐个对齐了这个文件夹下每个文件的修改点后,你就可以跟测试说:提测!