拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Git分支从原理到实战:合并、冲突与分支策略全解析

1. 别把 Git 当网盘分支到底是什么我见过太多团队Git 用得跟 Dropbox 似的所有人挤在 master 上每天 pull 一下改完就 push遇到要同时开发两个功能就手忙脚乱。这不是 Git 的问题是分支没玩明白。分支大概是 Git 里最值钱的设计它让并行开发这件事的成本降到了接近零。这篇文章不打算从 git init 开始啰嗦我默认你已经会 clone、commit、push但对分支的理解还停留在听说能开好几个线的水平。我会把分支的创建、切换、合并、冲突解决以及那些日常开发里高频出现的灵异事件一次讲透。先说清楚一个底层事实Git 的分支本质上就是一个指向提交对象的可变指针。当你执行git branch feature的时候Git 并没有复制任何文件它只是在.git/refs/heads/目录下新建了一个 40 位字符的文件里面存着当前提交的 SHA-1 值。所以创建分支的代价几乎为零——不管你的仓库是 1MB 还是 10GB创建分支都是瞬间完成。这一步理解了后面所有操作都不会懵。还有一个概念必须刻在脑子里HEAD。它是一个特殊的指针用来表示你现在在哪个分支上。当你切换分支时Git 做的实际上是两件事把 HEAD 从旧分支移动到新分支然后用新分支指向的那个提交去更新你的工作目录。我经常打一个比方分支是一根绳索HEAD 是你手里攥着的那端工作目录是你眼前看到的风景。你换一根绳索视野里的文件就跟着变了。明白了这一点很多奇怪的报错就能解释了。比如你明明git checkout feature成功了但文件看起来跟 master 一模一样——这大概率是因为 feature 分支就是从当前 master 的位置拉出来的它俩指向同一个提交文件当然一样。这不叫没切过去这叫两条分支暂时重合等你在 feature 上提交了新东西差异才会显现。1.1 分支的存储结构与三种引用类型如果你cat .git/HEAD会看到类似ref: refs/heads/master的内容这表示 HEAD 当前指向 master 分支。.git/refs/heads/下放着所有本地分支.git/refs/remotes/下放的是远程分支的快照指针比如 origin/master.git/refs/tags/下是标签。这三个目录你要分清楚尤其是远程分支和本地分支的区别origin/master不是你远程仓库里的那个分支本体而是你本地缓存的上次跟远程同步时远程 master 的位置。它只在 fetch 的时候更新所以你会看到本地 master 和 origin/master 的指针越走越远——这是正常的恰好说明你有未推送的本地提交或者未拉取的远程提交。还有个容易忽略的引用是HEAD~1、HEAD~2这种相对引用以及HEAD^父提交标记。分支合并之后HEAD~1和HEAD^指向的可能不是同一个提交——因为合并提交有两个父提交。这个细节在回滚操作时特别容易踩坑后面冲突解决章节我会再提。2. 创建与切换本地分支、远程分支和那些灵异事件分支操作的高频场景就那么几个从当前代码拉一条新线做功能开发从远程拉一条别人推上来的分支以及在不同分支之间来回横跳。每个场景都有对应的命令但真正让新手头疼的往往不是命令本身而是切换前后工作区、暂存区、仓库三者之间的关系。2.1 创建分支的三种姿势第一种最朴素git branch feature。只在当前提交上创建一个名为 feature 的新指针但不切换过去。你后续的提交还是落在原来的分支上。第二种最常用git checkout -b feature等价于git branch feature git checkout feature。创建并立即切换。Git 2.23 之后有了更语义化的git switch -c feature功能一样但字面意思更明确推荐新项目用这个。第三种从指定起点创建git checkout -b feature origin/dev。这不是从当前分支拉线而是以远程 dev 分支的最新位置为起点创建新分支。这个操作在团队协作里非常实用——你想基于别人的未合并代码做二次开发时就不用先切换到 dev 再拉分支了。创建分支之前最好先确认基线对不对。我见过有人想从 master 拉分支做新功能结果当前还停在另一个功能分支上git checkout -b feature拉出来的线带着一堆不属于自己的改动。正确的做法是先git checkout master git pull确保基线是最新的再git checkout -b feature。2.2 切换分支时工作区里的未提交改动去哪了这是出现灵异事件率最高的问题。假设你在 feature 分支上改了两个文件还没 commit这时候git checkout master结果分几种情况改动没冲突Git 会帮你把未提交的改动带到master 上。注意是带过去不是丢弃。改动有冲突Git 会拒绝切换报错Your local changes would be overwritten by checkout这是保护机制不是故障。改了 A 文件但 master 上 A 文件也被人改过同样拒绝。这个设计逻辑其实是尽量不让你丢工作。但很多人不理解以为切换分支就该像换房间一样彻底隔离发现改动跟过来了就觉得很诡异。实际上未提交的改动不属于任何分支它是游离在工作区里的状态。Git 切换分支时除非遇到内容冲突否则都会原样携带。所以正确的习惯是切换分支前要么 commit要么 stash。如果你手头的改动还不想提交用git stash把它存起来切过去干完活再git stash pop拿回来。开发 Git 时经常犯的一个错误就是在分支间带着半成品来回切早晚有一天把 A 分支的改动误提交到 B 分支上。关于 stash 多说一句git stash默认只暂存已跟踪文件的改动新文件untracked不在里边。想让新文件也一起存得用git stash -u。2022 年 Git 2.35 引入的git stash push也支持--include-untracked但很多人的 Git 版本还没更新直接用-u更稳妥。2.3 如何把本地分支和远程分支接上线从远程拉分支通常用git checkout -b feature origin/feature有的 Git 版本可以简化成git checkout feature——如果本地没有 feature 分支但远程有Git 会自动帮你建一个跟踪 origin/feature 的本地分支。这个行为在不同版本里表现有差异有的会提示你用--track。我推荐显式写法git fetch origin git checkout -b feature origin/featurefetch 先更新远程追踪分支的指针再基于它创建本地分支。好处是你知道自己在干什么不会因为自动匹配产生诶我这分支怎么跟着远程走的困惑。建立跟踪关系后日常git pull和git push就能自动匹配上下游不用每次写完整参数。查看跟踪关系用git branch -vv会列出每个本地分支和远程分支的对应关系以及领先落后状态。这个命令我在做仓库体检时必跑能一眼看出哪些分支长时间没有推送、哪些分支的远程上游已经被删了。2.4 图形化工具里的分支切换IDEA、VS Code、TortoiseGit命令行熟练的人一般不爱碰图形工具但团队里总有同事习惯用 IDE 或小乌龟。IDEA 的分支操作集中在右下角或顶部 Git 窗口点右下角分支名弹出列表选择New Branch可以创建并切换选Checkout切到已有分支Update Project对应 pull。IDEA 有一个比命令行直观的地方它会用颜色/标记显示每个分支相对当前分支的领先落后状态Merge 操作也有可视化预览。但 IDEA 右下角有个常见坑当前在某个分支上时右下角只显示该分支名有些人换了分支但没刷新视图以为没切换。实际上 IDEA 的文件树和编辑器会跟着分支切换自动刷新如果你编辑器里还开着旧分支的文件切过去之后会变成新分支的文件内容关上再打开就能看到。2024 版还出现过右下角分支信息不显示的情况重启 IDE 或者File - Invalidate Caches一般能解决。TortoiseGit小乌龟是 Windows 上最常见的 Git 图形客户端切换分支在右键菜单TortoiseGit - Switch/Checkout弹窗里选Branch再选目标分支就行。它的合并在TortoiseGit - Merge里冲突解决工具有点像左右对照 中间结果输出操作直观适合不喜欢命令行的同事。但小乌龟有它的毛病某些操作尤其 rebase 交互式不如命令行灵活所以如果你是团队里的Git 担当还是得把命令行的玩法吃透图形工具替你省不掉这一步。VS Code 的分支操作集中在左侧源代码管理面板底部状态栏显示当前分支名点一下就是分支切换列表号创建新分支三个点菜单里能找到 merge、rebase、stash 等操作。VS Code 的图形化冲突解决体验我认为是几个主流 IDE 里最好的它用当前更改 / 传入更改 / 合并结果三栏展示比起对着标记抠代码要舒服太多。3. 合并的艺术merge 与 rebase 怎么选到了正文戏。合并是分支存在的意义也是大多数 Git 疑难杂症的源头。在动手之前先搞清楚合并的本质把两条分支的分叉点之后的提交整合成一条线。3.1 Fast-forward 合并为什么有时候 merge 完没有合并记录当你从 master 拉出 feature提交了两次期间 master 一动不动此时git checkout master git merge featureGit 发现 master 是 feature 的祖先直接把 master 指针快进到 feature 指向的位置。这就是 Fast-forward快进合并。合并后 master 和 feature 指向同一个提交历史是一条直线没有产生合并提交。很多人第一次看到这个情况会慌我明明 merge 了怎么 log 里一点痕迹都没有放心这是正常且理想的合并方式历史最干净。想强制禁止快进、保留一个合并提交用git merge --no-ff feature。Git 默认的快进行为在合并 feature 分支时可以接受但在合并某些需要明确标记这是一次合并的长期分支比如 release 分支时团队往往约定用--no-ff这样后续回滚能精准定位到合并点。3.2 三方合并真正的合并是怎么发生的master 往前走了feature 也往前走了两条分支在分叉点之后都有各自的提交这时候快进失效Git 需要做三方合并取三个快照——公共祖先merge base、当前分支头ours、目标分支头theirs然后尝试把两边的改动叠加到公共祖先上。这个公共祖先很关键。它不是简单地对比两个分支头而是要在提交图里找到分叉点。Git 用了一个叫merge-base的命令来定位它git merge-base master feature输出一个提交 SHA。做过复杂合并的人可能遇到过某些Git 怎么这么蠢的时刻绝大部分原因是公共祖先定位得和你预期不一致导致补丁上下文对不上。三方合并的策略是逐文件逐块地做补丁叠加如果两边改的是不同文件的不同区域Git 能自动合并你不需要做任何事如果两边改到了同一文件的同一块区域Git 就会报冲突。这个策略决定了你手动解决冲突时的操作边界——你只需要处理真正重叠的部分其余 Git 已经帮你拼好了。3.3 实战把 dev 分支合并到 master这是团队协作里被问烂的题目怎么把 dev 分支提交到 master其实答案就一句话切到 master把 dev 合并进来再推送。git checkout master git pull origin master # 先跟远程同步避免本地 master 落后 git merge dev # 或 git merge --no-ff dev # 若有冲突解决后继续 git push origin master需要注意几点细节push 之前先 pull。如果远程 master 已经有别人的提交直接 push 会被拒绝non-fast-forward。如果团队约定 dev 只做集成、不允许直接改 master那就别在 master 上手工 merge应该走 Merge Request / Pull Request但这只是流程约束底层命令还是一样的。合并完成后git log --graph --oneline --all能清楚看到提交图。如果 merge 创建了合并提交图上会有M字样的分叉汇合点。3.4 rebase 到底改了什么别在共享分支上这么玩rebase 的核心操作是改变提交的基线。git rebase master在 feature 分支上执行的意思是把 feature 上从分叉点之后的提交一个个摘下来以 master 当前头部为新的基底重新应用一遍。效果上feature 分支像完全基于最新 master 长出来的历史是一条直线。和 merge 对比merge 保留你真实的历史分叉结构rebase 重写历史、让你看起来从未分叉过。两者没有绝对优劣看团队守则。我个人的经验是自己本地的功能分支还没推远程随便 rebase让历史干净。已经 push 到远程、且别人可能拉过这个分支绝对不要 rebase。rebase 会生成新的提交哈希等于把别人的历史地基换掉了别人下次 pull 会痛苦不堪。Git 2.36 之后我常配合git pull --rebase使用本地有未推送提交远程又有新提交直接 pull 会生成一个多余的 merge 提交用--rebase则把本地提交重放到远程提交之上历史整洁。但前提同样是只有本地独有的提交才适合这么干。如果想深刻理解 rebase 的危险性跑一次git reflog看看——rebase 之后原来的提交并不是被删了而是被新的提交对象取代旧对象还躺在对象库里一段时间git reflog能让你找回rebase 之前的状态。这也是很多人误解回滚的地方Git 的提交和分支并非不可撤销只是你需要知道去哪里找。4. 冲突解决实战从看懂冲突标记到拿捏三方合并只要团队超过两个人、评论区里多改了几次同一个文件冲突就一定会出现。怕冲突是没有必要的冲突不是事故而是 Git 在告诉你这里需要人类来做决定。真正要训练的是解决冲突的流程和心态。4.1 冲突标记到底在说什么当冲突发生时你的文件里会出现这种内容 HEAD 你的当前分支代码 目标分支代码 feature-branch三行分隔符把文件分成了上下两个区域 HEAD和之间是当前分支ours的内容到 feature-branch是目标分支theirs的内容。你的任务就是决定保留哪边、怎么融合然后把三行分隔符删掉。一个常见的低级错误是解决完冲突后忘了删掉//标记。Git 在提交时会检查文件里是否还有冲突标记但有些编辑器的高亮会掩盖它们人眼扫视容易漏掉。我建议解决完马上用grep -n ^\|^\|^ 文件名扫一遍确认。4.2 解决冲突的完整流程命令行版假设我在 feature 分支上工作要合并 master 进来这是常规操作把你的分支更新到最新基线git checkout feature git merge master # 输出CONFLICT (content): Merge conflict in src/index.js # Automatic merge failed; fix conflicts and then commit the result.接下来按部就班git status查看哪些文件冲突。处于 unmerged 状态的文件路径后面会标both modified。逐个打开冲突文件找到标记分析冲突上下文。手动修改文件确定最终内容删除所有冲突标记。git add 冲突文件告诉 Git 这个文件解决了。所有冲突文件都 add 完毕后执行git commit。合并提交的消息已经帮我们写好了直接保存退出即可。过程中的排查心态很重要冲突文件可能有好几个有些你根本不知道它为什么冲突。这时候git log --oneline --all --graph看提交图git log -p HEAD..feature看 feature 上自分叉点以来的具体改动比盲猜高效得多。尤其看到别人的提交改了什么能帮你判断他那边的改动意图。4.3 使用图形化工具解决冲突SourceTree、VS Code、IDEA、TortoiseGit不想手工抠标记的话首选 VS Code。冲突文件打开后编辑器顶部会有Accept Current Change、Accept Incoming Change、Accept Both Changes三个按钮下方是合并结果的实时预览。它的 Accept Both Changes 在有上下两块代码、你都想保留时非常实用但注意它只是简单拼接如果两边代码有逻辑依赖还得手动调整顺序。SourceTree 的冲突解决入口在Actions - Resolve Conflicts会列出所有冲突文件你可以选Resolve Using Mine全取当前分支、Resolve Using Theirs全取目标分支或打开外部合并工具精细处理。Team 环境里比较常见的问题是 SourceTree 和外部合并工具比如 Beyond Compare、Kaleidoscope的集成配置装好之后记得在偏好设置里指定一次路径。IDEA 的冲突解决窗口是我个人觉得最有确定感的左侧是本地版本右侧是远程版本中间是结果。每个冲突块可以左右一键选择也可以直接在中间栏编辑。解决完点 ApplyIDEA 会把结果写回文件并自动 stage。注意 IDEA 默认显示的是Accept Yours/Accept Theirs之类的命名不同版本文案略有差异但逻辑相通。TortoiseGit 解决冲突的体验右键冲突文件 -Edit conflicts弹出 TortoiseGitMerge三栏视图不需要我多解释跟其他工具差不多。这里要提醒一点TortoiseGitMerge 保存后还需要回到 TortoiseGit 里执行Mark as resolved不然 Git 仍认为文件处于冲突状态。4.4 一个真实冲突案例从报错到解决讲一个最近让同事卡了半小时的案例。他和我在同一天改了config.js里的 API 配置——他加了新的接口前缀我调整了超时时间。合并时冲突标记把整个 config 对象都圈了起来因为改动行离得近Git 的 diff 算法把两个改动判定为同一块。我的做法是打开文件看范围发现他改的是baseURL我改的是timeout两个字段根本不相干。这种情况不需要二选一直接把冲突标记删掉、保留两边内容就行——因为本质上两边改动并不矛盾是 Git 的块匹配粒度不够细而已。这就是我想强调的经验冲突标记的范围不等于真正矛盾的代码范围。Git 是按 diff 块来匹配的有时会把相邻但不相干的改动圈进同一个冲突。遇到大段冲突先别急着删逐行看标记里的内容搞清楚它是因为修改了同一行还是修改了相邻行。很多时候解决方案不是保留一边、丢弃另一边而是两边都留着再微调。4.5 冲突之后怎么避免下一次解决冲突只是止血更重要的是为什么经常冲突。常见根源有三个文件职责不清每个人都在改同一个万能配置文件或公共工具类自然天天撞车。解法是拆分文件、各自独立或者把频繁变动的配置搬到环境变量/配置中心。长期不更新分支feature 分支拉出来之后一两个月不 merge master再合并必然大冲突。解法是高频地git merge master或git rebase master回主线把小冲突提前拆解成多次小冲突。格式风格不统一IDE 自动格式化导致整个文件 diff 巨大。解法是统一.editorconfig和 prettier 配置或者约定不批量格式化历史文件。5. 分支的日常维护删除、储藏、回滚与远程同步分支创建多了仓库就会变成一团乱麻。这一章说点用的频率高、但很多人操作不对的维护动作。5.1 删除本地分支和远程分支别把命令记混本地分支删除git branch -d feature # 安全删除仅当该分支已合并 git branch -D feature # 强制删除无论是否合并-d会检查分支是否已合并到当前分支没合并的话拒绝删除防止你丢掉未合并的提交。-D是--delete --force的简写跳过检查慎用。我见过有人顺手-D删掉了一个还没 merge 的 feature 分支里面有个重要提交没推远程最后靠git reflog才找回来。能找回是万幸但不能把能找回当成随便删的底气。远程分支删除git push origin --delete feature这个命令跟本地删除完全是两码事——它是在远程仓库上删除分支。Git 没有单独的远程分支删除命令只能用 push 加--delete老版本用git push origin :feature的空推送语法现在也能用但可读性差。还有一类残留分支要关注远程分支别人已经删了但你本地还能看到origin/feature。这时候git remote prune origin能清理掉本地过期的远程追踪分支更常见的做法是git fetch --prune每次 fetch 时顺手清理。VS Code 用户可以在源代码管理面板的刷新按钮附近找到清理操作IDEA 则在 Git 窗口里有对应的 prune 选项。5.2 stash 进阶不只是存一下那么简单前面提过git stash -u这里展开讲讲 stash 的常见组合。git stash save wip: 重构登录模块可以给储藏命名方便之后区分git stash list查看储藏列表git stash show stash{0}看某条储藏改了哪些文件git stash pop应用最近的储藏并从列表里移除git stash apply应用但保留储藏。stash 应用有时也会冲突——分支在储藏之后继续演化你存的改动和当前分支新内容打架。解决方式和普通冲突一模一样改完git add即可。有个冷门但好用的点git stash branch new-branch可以在新分支上应用储藏专治我在错误分支上做了改动、想换个分支重开的场景。5.3 提交了不想提交的分支改错分支时的补救操作最常见的中级事故是工作区里改了一堆文件想切分支时被 Git 拦住一不做二不休直接在 master 上 commit 了但那一堆改动本应属于 feature。补救方式取决于你是否已经推送没推送的情况下我通常这样操作git reset --soft HEAD~1 # 撤销提交、保留改动到暂存区 git stash -u # 把改动暂存起来包括未跟踪文件 git checkout feature # 切换分支 git stash pop # 把改动恢复到工作区--soft是关键它只移动分支指针不动工作区和暂存区等于把提交拆散回改动。这样不会丢任何内容比--hard安全得多。如果你已经推送了过程复杂一点通常走 revert 或者 reset --hard force push后者要跟团队协商因为会覆盖远程历史非常危险非必要不用。5.4 远程同步的几种场景处理日常开发里你可能会用到这些本地落后、想要最新远程代码git pull --rebase本地无提交或提交少时推荐或git pull接受自动生成 merge 提交。本地领先、想推送git push。远程也被别人推过就会被拒绝先 pull 再 push。想把本地分支推到远程并设为上游git push -u origin feature之后git push/git pull就自动关联了。只想更新远程追踪指针、不合并git fetch。这个命令干净地更新origin/*不动工作区。团队协作里频繁用到的一种场景是把别人推上来的远程分支拉到本地。git checkout -b feature origin/feature之后你可以在这条分支上开发、push其他人也能再拉。如果你只是想看看远程分支上有什么不想切换过去用git log origin/feature --oneline -5或git show origin/feature就能浏览不用创建本地分支。6. 实际项目中的分支策略与避坑清单最后聊点项目层面的东西。分支命令本身不难难的是团队怎么约定分支的使用规则。没有规则的仓库合并几次就乱成粥了。6.1 三种主流分支策略怎么选Git Flow重型流程。master主分支、develop集成分支、feature/功能分支、release/发布分支、hotfix/*热修复分支。适合有明确版本规划的正式产品对团队成员纪律要求高。GitHub Flow轻量流程。master 永远可部署所有改动走 feature 分支 Pull Request。适合持续交付的互联网产品团队反馈节奏快。GitLab Flow介于两者之间。引入 environment branches比如 production/staging适合有明确环境区分、需要多环境部署的项目。对大多数中小团队我推荐从 GitHub Flow 起步master 受保护feature 分支拉取开发合并走 MR/PR 并强制 code review。别一上来就搞 Git Flow那种复杂度是给多人、多版本、多环境的大项目准备的小团队照搬只会被流程拖垮。6.2 分支命名规范建议分支命名看起来是小事但长期协作中影响很大。我自己和团队约定的是类型/描述前缀式命名比如feature/xxx、fix/xxx、refactor/xxx、docs/xxx描述用短横线连接。这样git branch列出来能一眼看出每条分支是干什么用的MR 列表也清爽。如果有需求单号比如 JIRA可以带上feature/PROJ-123-add-login-page。命名规范最好写进仓库根目录的CONTRIBUTING.md作为团队约定的一部分。6.3 高频问题速查表把我这些年被问得最多的问题整理成一张表各位可以直接对照使用现象原因解决切换分支后文件变不回去了未提交的改动被带到了目标分支切回原分支看改动或git stash pop以后切分支前先 commit/stash提示local changes would be overwritten目标分支与当前工作区改动冲突先提交或 stash别强切git pull后历史多了个 merge 提交默认 pull 执行 merge用git pull --rebase让历史线性远程分支被别人删了本地还在远程追踪指针过期git fetch --prune或git remote prune origin合并后发现有文件没修改却显示冲突行尾符或文件权限差异检查.gitattributes的textauto配置和core.filemode false不小心 commit 错分支提交落在了当前分支上git reset --soft HEAD~1 stash 切分支 pop大幅改动冲突标记难解决格式问题或文件职责不清考虑统一格式化或拆分文件职责用图形工具辅助本地分支明明删了 push 却说不是 upstream本地分支和远程追踪关系残留git push origin --delete远程分支git remote prune origin清本地跟踪6.4 我给新人的三条实操建议第一把git log --graph --oneline --all当成你的第二个脸。每次操作前看一眼提交图你就能直观理解当前处于哪条线上、分叉在哪、合并要做什么。很多人操作 Git 像在盲人摸象问题就出在脑子里没有这张图。第二遇到不确定的情况先git status再git reflog。前者告诉你此时的状态后者告诉你历史上的每一步操作。你要是能用 reflog 把自己从rebase 把分支搞没了的恐慌里救回来一次就再也不会怕 Git 了。第三别在共享分支上随便 reset --hard。reset --hard 是真的会丢东西的操作虽然 reflog 还能临时找回在 master 这种多人协作的分支上尤其危险。回滚文案、修改历史前先问同事这个分支还有人用吗。最后分享一个我自己的小偏好解决冲突这块项目里不同编辑器的人混用也没关系但建议团队里至少有两个人能把命令行冲突解决玩利索。图形工具再好用遇到冲突标记错乱、三方合并边界模糊的极端情况最终还是得靠命令行回到提交图底层去分析。这不是让你一定要用命令行的意思——工具是服务人的选顺手的用就好但理解底层原理可以让你在任何图形工具面前都不慌。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门