首先,当你读到这篇文章的时候,可能已经进入到这个需求的场景了,但笔者还是想构建一个常见的业务场景,以希望读者能够更快的进入到这个问题背景中:
在一个岁月静好的一天,作为开发的你来到工位,看了看项目计划和待办事项,你发现,需要按顺序完成两个需求:
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 [][--]
即:用于拿暂存区的文件覆盖工作区的文件,或者用指定提交中的文件覆盖暂存区和工作区中对应的文件。
是可选项,如果省略则相当于从暂存区(index)检出。这和 git reset 重置命令(例如 git reset HEAD )大不相同:重置的默认值是 HEAD,而检出的默认值是暂存区。因此重置一般用于重置暂存区(除非使用--hard 参数,否则不重置工作区),而检出命令主要是覆盖工作区(如果不省略,也会替换暂存区中相应的文件)。
该命令(包含了路径 的用法)不会改变 HEAD 头指针,主要是用于拿指定版本的文件覆盖工作区中对应的文件。如果省略,则会拿暂存区的文件覆盖工作区的文件,否则用指定提交中的文件覆盖暂存区和工作区中对应的文。
举个例子:
如果要放弃修改工作空间内容:
在 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 命令进行知识点的补充,想直接看方案的可以略过===
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。