Git多人开发教程-精华版
理解Git核心:Commit的本质与增量思维
🔍 Commit的本质:增量而非快照
多数教程将Git的commit
描述为文件快照,但更本质的理解是:每次commit都是基于上一次提交的增量更新。这种视角让Git提交树的理解更加清晰:
分支的本质就是基于某个commit的不同增量更新路径
📖 文档演进的增量示例
想象一个空白Word文档的版本演进:
- 小A的线性提交
- 写了第一段 →
commit 1
- 添加第二段 →
commit 2
- 完成第三段 →
commit 3
gitGraph commit commit commit
- 分支的增量分离
小美和小帅基于commit 3
创建分支,小美加了封面,小帅加了目录:
gitGraph commit id: "v1" commit id: "v2" commit id: "v3" branch feature-design branch feature-toc checkout feature-design commit id: "封面Δ" checkout feature-toc commit id: "目录Δ"
- 小美分支 =
v3 + 封面Δ
- 小帅分支 =
v3 + 目录Δ
🔀 合并的本质:增量融合
gitGraph commit id: "v3" branch 小美feature-design commit id: "封面Δ" type: HIGHLIGHT branch 小帅feature-toc commit id: "目录Δ" type: HIGHLIGHT checkout main merge 小美feature-design id: "合并封面" merge 小帅feature-toc id: "合并目录"
合并操作 = 将两个增量(封面Δ + 目录Δ)叠加到基础版本(v3)
⚠ 冲突的本质:增量重叠
当两人同时修改同一区域时:
小美的增量:认为第二段没有存在的必要,删除第一面的第2段
小帅的增量:重写了第2段,优化了文字逻辑
Git无法自动融合冲突的增量Δ,需要手动决策:
1 | <<<<<<< HEAD |
🛠 强烈建议自己动手构建提交树,尝试一下这个过程
gitGraph commit id: "init" type: NORMAL tag: "初始化仓库" commit id: "A1" type: NORMAL tag: "提交1:添加第一段" commit id: "A2" type: NORMAL tag: "提交2:添加第二段" branch feature-cover checkout feature-cover commit id: "B1" type: NORMAL tag: "分支:加封面" checkout main branch feature-toc checkout feature-toc commit id: "C1" type: NORMAL tag: "分支:加目录" checkout main merge feature-cover id: "合并封面" merge feature-toc id: "合并目录" branch conflict-demo checkout conflict-demo commit id: "D1" type: NORMAL tag: "冲突:同一段不同修改" checkout main merge conflict-demo id: "解决冲突"
关键学习路径
- 在空仓库中构建3-4次提交
- 创建分支并制造差异修改
- 故意设计重叠修改引发冲突
- 自己尝试解决冲突
没有实操的Git学习如同纸上谈兵!
理解了 commit 之后,实际上对于所有的命令,都能够在任何其他教程中快速获得正确认知。接下来我介绍一些非常常用的命令,仅提供最核心的用法和注意事项。
🚀 从 git clone 说起
我们都知道 git clone
是克隆了一个远程仓库到本地。实际上它最核心的意义除了从远程获取一份代码之外,最重要的是我们建立了自己的代码与远程代码仓库的关联。
这意味着我们在远程维护了一个公共的版本,每个人自由的在这个版本上进行增量更新。
我们最简单而常见的方式是:
- 远程维护着一个主分支
main
(稳定的版本) - 每个人在本地分支上进行开发
- 推送你的本地分支到远程仓库
- 通过发起一个合并(Pull Request)将你的修改合并到主分支
本质上就是申请把你基于主分支的增量更新合并到主分支上
💡 实际开发示例:添加按钮功能
以增加一个按钮为例,实际上进行开发时,你只需要如下考虑:
开发流程
- 确定基础分支
我该基于哪个分支进行增量更新?
一般都是稳定的 main
分支,那么我们就把 main
分支拉取到本地(pull
),然后你基于本地的 main
分支,创建一个新的按钮开发分支(不妨叫 feature-addButton
)。
1 | git pull origin main |
然后我们在本地的这个分支上完成开发,提交(commit
)后得到的提交记录里就是这个按钮的修改增量。
- 合并到稳定版本
我该如何合并到最新的稳定版本?
我们只需要把你完成开发的那个按钮分支推送(push
)到远程仓库,然后在远程仓库上从你的远程特性分支发起一个合并请求(Pull Request)。
这个请求在通过后会把你基于 main 分支的增量更新合并到 main 分支上。
- 处理冲突
如果有冲突怎么办?
例如你和其他人同时修改了同一区域,且其他人比你先完成了一次远程的 PR 合并,此时 Git 会提示你的 PR 出现了冲突。
你需要手动解决冲突,选择保留哪个增量或者融合两个增量。
- 冲突解决方案
冲突到底如何解决呢?
这里也是最吊诡的地方,大多数教程总是相当含糊,或者说堆砌术语,没有讲清楚解决冲突的本质。
⚠️ 解决冲突的本质
🔧 两种冲突解决场景
📋 场景一:推送前预先解决冲突
最佳实践:在推送分支到远程前,主动更新到最新版本
操作步骤:
- 更新本地
main
分支到最新版本 - 将特性分支 rebase 到最新的
main
分支
1 | git checkout main |
🚨 场景二:PR发现冲突后解决
如果你已经推送了分支并发起了PR,但发现有冲突,需要更新远程分支
完整操作流程:
1 | git checkout main |
通过这种方式,你的PR依然存在,但此时就能够无冲突地合并到main分支了。
📊 Rebase 过程可视化
gitGraph commit id: "C1" tag: "main基础版本" commit id: "C2" tag: "其他人的提交" branch feature-yours checkout feature-yours commit id: "F1" tag: "你的功能开发" commit id: "F2" tag: "完善功能" checkout main commit id: "C3" tag: "其他人先合并了" commit id: "C4" tag: "main最新版本" checkout feature-yours commit id: "F1'" tag: "rebase后: 基于C4的F1" commit id: "F2'" tag: "rebase后: 基于F1'的F2" checkout main merge feature-yours tag: "无冲突合并"
🔍 Rebase 的本质
将你的增量更新重新应用到最新的base上
- 原来:你的分支 =
C2 + F1Δ + F2Δ
- rebase后:你的分支 =
C4 + F1'Δ + F2'Δ
- 其中
F1'
和F2'
是基于新base(C4)重新计算的增量
git stash - 暂存工作区
这个命令非常常用!当你开发到一半需要切换分支修 bug,但又不想提交未完成的代码时:
1 | git stash # 暂存当前修改 |
对于经常需要切换的配置文件,可以创建带标识的暂存:
1 | git stash save "配置文件修改" |
git cherry-pick - 复制提交
注意:复制的是修改增量,可能会产生冲突
用于将某个分支的提交复制到当前分支:
1 | git checkout dev |
使用场景示例:
- 新功能在
feature
分支开发完成 - 需要在
dev
分支测试 - 使用 cherry-pick 将功能提交复制过去
git push --force-with-lease - 安全强推
相比 --force
更安全的强制推送方式:
为什么更安全?
如果其他人在你的提交之后进行了修改,此操作会失败,避免覆盖他人的工作
1 | git push --force-with-lease |
适用场景:处理 GitHub PR comments 或 merge conflict
git reset - 重置提交
1 | git reset --soft HEAD~n |
将最新的 n 个提交退回到工作区,常用于修改上次提交
1 | git reset --hard HEAD~n |
危险操作:会直接丢弃这些修改,请谨慎使用!
git rebase -i - 交互式变基
进阶功能:建议有一定经验后使用
功能包括:
- 🔄 修改 commit 提交顺序
- 🔗 合并多个提交
- ✏️ 修改历史提交信息
- 🗑️ 删除某次历史提交
使用注意事项:
由于会改变 Git 历史,在协作开发中需要谨慎使用:- ✅ 自己的特性分支且独立开发:可以使用
- ❌ 多人协作的共享分支:避免使用
- ⚠️ 使用后需要强制推送到远程仓库
1 | git rebase -i HEAD~n |