拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

6篇文章讲清楚Git:Git 分支与合并:一次讲清 HEAD、merge 和 rebase (2/6)

6篇文章讲清楚Git:Git 分支与合并:一次讲清 HEAD、merge 和 rebase (2/6) 上一篇我们把提交讲成了一串用parent指针串起来的快照链但那只是一条线实际开发中我们不可能大家都在一条线上改代码所以这就引出了分支。分支就是一个指针理解分支最重要的一句话是分支什么都没多存它就是一个指向某个提交的可变指针。在.git/refs/heads/下面一个分支就是一个普普通通的文本文件里面存着 40 个字符的哈希cat.git/refs/heads/main5cccce7a1f3b2c8d9e0f1a2b3c4d5e6f7a8b9c0d就这么简单。所以 Git 建一个分支本质上只是写了一个 41 字节的文件瞬间完成这也是它比 SVN 建分支快几个数量级的根本原因SVN 的分支是真的去复制一份目录出来。A --- B --- C ↑ main main 这个分支指向 CHEAD 是什么HEAD 是我们现在在哪儿的标记唯一需要注意的是它通常不直接指向提交而是指向一个分支cat.git/HEADref: refs/heads/main这叫符号引用意思是 HEAD 指向main这个分支然后main再指向提交 C所以 HEAD 其实是个两层的指向。搞清这一层之后一个新提交产生的过程就很好理解了Git 做的事情是把 HEAD 指向的那个分支往前移一格HEAD 本身纹丝不动它还是指着main。所以切换分支本质上只是改了这个 HEAD 文件的内容不是去搬动什么代码。游离 HEAD如果 HEAD 不指向分支而是直接指向某个提交这种状态就叫游离 HEADdetached HEADgitcheckout HEAD~1HEAD detached at 3520544 nothing to commit, working tree clean这时候我们就是在看一个历史提交HEAD指向3520544这个具体提交而不指向任何分支。可以拿来做实验但如果在这个状态下提交了这些提交不属于任何分支等我们切走之后就没有任何引用指向它们了很容易找不回来除非用reflog这个到第 4 篇讲gitcheckout-qHEAD~1echoxx.txtgitadd.gitcommit-m游离状态下的提交gitcheckout mainWarning: you are leaving 1 commit behind, not connected to any of your branches: 52393a9 游离状态下的提交Git 会好心地提醒我们你丢下了一个提交。想保留的话在切走之前先建个分支就行了git checkout -b new-branch。分支的增删改查查看分支gitbranch# 本地分支当前分支前面有 *gitbranch-v# 带上每个分支的最后一个提交gitbranch-vv# 再带上上游分支第 3 篇会用到gitbranch-a# 包括远程分支gitbranch--merged# 哪些分支已经合并到当前分支了创建和切换gitbranch feature# 只创建不切换gitcheckout feature# 切换gitcheckout-bfeature# 创建并切换最常用gitswitch-cfeature# 同上新命令这里需要注意的是git checkout -b feature是从当前 HEAD 的位置分出去的如果想从别的地方分就在后面跟上起点gitcheckout-bhotfix main# 从 main 分出一个 hotfixgitcheckout-bhotfix a1b2c3d# 从某个提交分删除分支gitbranch-dfeature# 删除如果还没合并会拒绝防止误删gitbranch-Dfeature# 强制删除不管有没有合并-d拒绝删除一个还没合并的分支这个设计是个保护机制。比如一个做到一半的feature分支直接删掉那些提交就真找不回来了其实reflog还能救见第 4 篇。确定不要了再用-D。checkout 和 switchgit checkout是个身兼数职的命令它既能切分支也能恢复文件gitcheckout main# 切分支gitcheckout -- a.txt# 把 a.txt 恢复成暂存区的样子恢复文件gitcheckout HEAD~1# 游离到某个提交同一个命令名承担了语义完全不同的操作其中git checkout -- a.txt这种还是会丢改动的危险操作很容易手滑敲错。所以 Git 2.23 把这两个职责拆成了两个专用命令老写法新写法干什么git checkout branchgit switch branch切分支git checkout -b branchgit switch -c branch建并切分支git checkout -- filegit restore file恢复文件丢弃工作区改动git checkout commitgit switch --detach commit游离到某个提交checkout现在还能用但新写的东西建议用switch和restore语义清楚也不容易误操作。合并现在假设有两个分支各自往前走了要把feature合进maingitswitch maingitmerge feature合并的结果有两种取决于两个分支的位置关系。快进合并如果main从分出去之后一次提交都没有那它的 HEAD 只是停在一个老位置上而feature是在它前面一路走下来的合并前 A --- B(main) --- C --- D(feature) 合并后 A --- B --- C --- D ↑ main, feature这种情况下 Git 不需要真的合并什么只要把main指针挪到D就行了这就叫快进fast-forward没有产生新的提交历史是一条直线。合并提交如果两个分支都往前走了那就分叉了没法靠移动指针解决C --- D (feature) / A --- B \ E --- F (main)这时候git merge feature会创建一个合并提交merge commit它有两个父提交gitmerge --no-ff feature-m合并 featuregitlog--oneline--graph看这个图|/是分叉点A两条线分别是feature的提交C和main的提交D最后|\汇合到合并提交5cccce7。--no-ff的意思是就算能快进也强制生成一个合并提交。为什么要这样因为快进之后历史里看不出这里曾经有个 feature 分支以后想整个回退这个功能就很麻烦得一个个提交去翻。有了合并提交回退功能只要 revert 这一个提交就行。merge 和 rebase 的区别这是分支里最需要搞清的一对也是面试和实际工作中都绕不开的。merge保留真实的分叉上面看到的那个图就是 merge 的结果。分叉在开发过程中是真实存在过的所以 merge 也如实地把它留在了历史里。好处是历史如实反映了当时发生了什么能看出来这些提交是并行开发的。坏处是分支一多图上全是分叉和合并git log会变得很难看。rebase把提交搬过去同样是把feature合进mainrebase 的做法完全不一样gitswitch featuregitrebase maingitswitch maingitmerge --ff-only feature* f3af094 C: feature 的提交 * 3520544 D: main 的提交 * 113a797 A: 初始提交完全没有分叉是一条直线。rebase 做的事情是把feature上的提交一个个拿下来先让feature指到main的最新提交再把刚才拿下来的提交在main最新提交的基础上重新应用一遍。效果看起来就像我本来就是在main最新的基础上开发的。代价是提交被改写了注意上面那个C提交的哈希merge 版本里是d41640drebase 之后变成了f3af094。改动内容完全一样但 commit 的哈希变了。原因就在第 1 篇讲的对象模型里commit 对象存着parent指针父提交变了对象内容就变了哈希自然跟着变。所以 rebase 本质上是产生了一个全新的提交原来那个在历史里消失了。这就是那条黄金法则的由来不要 rebase 已经推送到公共分支的提交。因为别人手里拿着的还是旧的那个提交旧的 SHA我们这边变成了新的两边一比对就是两条不同的历史别人拉取的时候要么报冲突要么被迫跟着改团队里会乱成一团。判断标准很简单这个分支除了我还有别人在用吗。只有自己在用的分支本地分支、自己的 PR 分支可以随便 rebase公共分支main、develop永远不要动。那到底该用哪个场景用哪个把功能分支合进 mainmerge --no-ff保留功能的边界功能分支开发期间同步 main 的最新代码rebase main让自己这边跟上避免以后攒出一次大冲突本地整理乱糟糟的提交rebase -i第 5 篇讲公共分支之间merge绝不要 rebase实践中一个比较常用的组合是开发期间用自己的分支rebase main保持同步这时候分支只有自己用安全功能开发完了合进main的时候用merge --no-ff留下一个合并提交作为这个功能的边界。冲突怎么处理冲突发生在同一个文件的同一段内容两个分支各改了一份的时候。Git 不知道该听谁的就交给我们来决定gitmerge featureAuto-merging a.txt CONFLICT (content): Merge conflict in a.txt Automatic merge failed; fix conflicts and then commit the result.打开a.txt会看到这样的标记 HEAD main 这边改成这样 feature 这边改成这样 feature三段的分工是 HEAD到之间当前分支合并时我们所在的分支也就是 HEAD的内容到 feature之间被合并分支的内容处理方式就是手工把整个冲突块编辑成我们想要的样子然后把那三行标记删掉。比如最终想两边都保留就改成main 这边改成这样 feature 这边改成这样改完之后gitadda.txt# 告诉 Git 这个文件的冲突我解决了gitcommit# 完成合并merge 会自动生成好提交说明git status会一路提示还有哪些文件没解决、下一步该干什么跟着走就行。想反悔怎么办合并和 rebase 中途都可以中止回到操作之前的状态gitmerge--abort# 放弃这次合并回到 merge 之前gitrebase--abort# 放弃这次 rebasegitcherry-pick--abortrebase 过程中还有另外两个参数gitrebase--continue# 解决完冲突继续应用下一个提交gitrebase--skip# 跳过当前这个提交继续下一个需要注意的是 rebase 的冲突处理和 merge 不太一样merge 只解决一次冲突提交完就结束了而rebase 是逐个提交重放理论上每一个提交都可能要解一次冲突所以 rebase 一个提交很多的分支会比较烦。这也是大分支同步主干时定期小步 rebase 比攒到最后一次性 rebase 舒服得多的原因。
返回列表