develop 的指针已经被移动到 feature 分支的最新提交上,提交历史因此呈现为一条直线。
事情就是这么回事儿:有人把 feature 合并到 develop,且这两条分支在 feature 建立之后,develop 没有再新增提交。Git 看到这种关系,就没必要造个新节点来“合并”,只要把 develop 这个指针直接挪到 feature 的最新提交上就行了。这种操作叫“快进”,看起来像是 develop 本来就是沿着 feature 前进的,历史纪录没有多出来的合并点。
能不能走快进路子,跟你合并前工作区和索引的状态有关系。更具体点儿:如果 develop 的末端恰好是 feature 的祖先提交,那就可以快进;如果两边各自都有新提交,Git 就不能简单把指针挪过去,需要进行三路合并,最后会产生一个带两个父提交的合并提交,那样历史就不是一条直线了。把这两种情形放到日志里看,差别一目了然:快进看不到合并节点,三路合并会留下那个合并提交作为痕迹。
说到 HEAD,得把它讲清楚。HEAD 实质上是个位置指示器,它指向当前检出的分支引用,也就是说你目前站在哪条分支上。你用 git checkout 或 git switch 切分支时,HEAD 指向的是分支名,分支名再指向对应的提交。好处是你提交时分支引用会自动往前走,历史连续。如果你直接检出某个提交哈希,HEAD 会变成“游离”的状态——它指向的是那个具体提交而不是分支名。那时候你若继续提交,新提交不会挂在某个分支上,容易造成提交“漂走”,要注意不要忘了切回分支把这些提交合并进来。
把 feature 合并进 develop,常见做法有好几种,大家平时会用到的实操命令我也说清楚:
- 在 develop 分支上,工作区干净的前提下,直接 git merge feature。Git 会先检查两条分支的关系,能快进就快进,不能就发起三路合并。
- 想强制保留一个合并提交,即便能快进也要显示地标注合并边界,就用 git merge --no-ff feature。这样会在 develop 上创建一次新的合并提交,方便后来通过合并点追溯某个功能的所有提交。
- 如果两边都有新提交,git merge 会启动三路合并流程,可能遇到冲突,需要人手去解决,解决完再提交合并结果。
操作步骤一般是这样的:切到 develop,确认 git status 显示工作区和暂存区都干净;执行 git fetch 把远端更新拉下来,必要时做个 git pull;接着 git merge feature 或者选择 --no-ff。Git 会判断能不能快进,能就把 develop 的引用移过去,并更新工作树到那次提交;不能就进入合并流程,按提示解决冲突,最后提交合并。别省这几步,尤其是在团队协作下,没拉最新就合并常常出问题。
团队为什么会有快进和非快进的差异,一般跟分支策略有关。有的团队喜爱把历史弄得很干净,要求先把 develop 拉最新或者让 feature 先做 rebase,把自己的提交变到 develop 后面,再合并,这样合并时就会快进;另一类团队偏好明确标注功能边界,统一在合并时用 --no-ff,让每个功能都有一个合并节点,便于后来审计和回滚。两种思路都有道理:线性的历史看上去清爽,但失去了分支是个“包裹”的直观痕迹;保留合并提交能清楚标识某次功能的范围,但日志会多出些合并节点。
在 GitLab 里能把这些行为配置好。项目设置里有合并请求的合并策略选项,可以设成允许快进、只允许合并提交、或者先 rebase 后合并等。管理员把团队达成的规则在项目设置里勾上,网页端点击“合并”时就会按规则走,能减少大家手动操作时踩坑的几率。把流程和 CI 配合起来——列如在合并前必须通过 CI、必须通过代码审查、必须拉齐远端——这些配合能把历史和代码质量都捋顺一点。
顺便提醒一点常见的踩坑:偶尔有人检出某个旧提交去看东西,一忙忘了切回分支就开始提交,结果那些提交跑到游离 HEAD 上。找回并不是没办法,但麻烦。另一个容易忽视的点是合并前的工作区状态,未提交的改动会让合并复杂化。养成好习惯:每次切分支前看一眼 git status,合并前做个 git fetch 和 git pull,把远端状态拉齐,这样出问题的概率就低许多。
如果你们团队想统一做法,这里给几条比较实用的提议:把合并策略写到团队文档里,CI 把 merge 时的检查放进 pipeline,遇到需要保留合并节点的功能就用 --no-ff,想要线性历史的就提前要求 rebase。再加上一条人性化的提醒——谁在合并前碰到冲突或是变更特别多,别独自硬干,多和同事沟通一下,省得把大家的历史弄得乱七八糟。