2.9 Git 版本控制基础
预计阅读时间:17 分钟
📖 目录
学习目标
学完本章后,你将能够:
- 理解 Git 三区域模型(工作区、暂存区、仓库)的工作原理
- 掌握日常开发的核心循环:修改→暂存→提交→推送
- 熟练创建、合并、删除分支并解决合并冲突
- 使用远程仓库协作:clone、push、pull、fetch
- 运用 stash、cherry-pick、reflog 等救命命令应对意外场景
核心知识
- 版本控制(Version Control)——跟踪文件变更历史,支持回滚、分支协作和多人并行开发
- 仓库(Repository)——Git 存储所有版本数据的目录,包含
.git/目录下的完整对象数据库 - 提交(Commit)——仓库中的一个快照,包含文件状态、作者、时间戳和父提交引用
- 分支(Branch)——指向某个提交的可移动指针,默认分支名为
main(或master) - HEAD——指向当前分支最新提交的特殊指针,表示"当前工作位置"
- 远程仓库(Remote)——托管在另一台机器上的仓库副本,通常用
origin作为默认别名 - 合并(Merge)——将两个分支的历史合并为一个,自动生成合并提交
- 变基(Rebase)——将一个分支的提交移植到另一个分支顶端,保持历史线性
知识关联
- 前置知识:熟悉 1.9:重定向与管道 管道与重定向、1.3:第一次进入命令行 命令行基本操作
- 后续影响:Git 是 4.18:Git 进阶 Git 进阶、4.9:CI/CD 持续部署 CI/CD 和 6.7:GitOps 入门 GitOps 的基石
- 配套技术:GitHub/GitLab/Gitee 等代码托管平台
- 相关章节:3.6:MySQL 数据库 Shell 脚本基础中编写可被 Git 跟踪的脚本;5.2:Docker 生产实践 脚本调试中的版本控制实践;6.12:AI 基础设施 Shell 脚本工程化中 Git 与自动化的集成
原理讲解
Git 三区域模型
Git 的核心设计围绕三个区域:工作区(Working Directory)——你实际编辑文件的地方;暂存区(Staging Area / Index)——准备纳入下一次提交的文件快照列表;仓库(Repository / .git)——存储所有历史提交的对象数据库。
工作区 暂存区 仓库
│ │ │
├── file.txt (v3) ──→ ├── file.txt (v3) ──→ ├── commit abc123 (v3)
│ │ ├── commit def456 (v2)
│ │ └── commit ghi789 (v1)
└──── git add ───────→ └─── git commit ────→
这种设计让你可以精细控制每次提交的内容——只提交部分文件的部分改动,而非全部。这也意味着文件在三个区域中的版本可能不同,git status 和 git diff 就是用来对比这些差异的。
提交对象的有向无环图(DAG)
Git 不是存储文件差异(diff),而是存储完整的文件快照(blob 对象)。每个提交对象包含:一个指向树对象的指针(文件清单)、指向父提交的指针、作者/提交者信息和提交消息。这些提交通过父子关系构成一个有向无环图(Directed Acyclic Graph, DAG),分支只是指向图中某个节点的标签。
Merge 与 Rebase 的区别
| 操作 | 结果 | 优点 | 缺点 |
|---|---|---|---|
git merge | 创建合并提交,保留完整历史分支结构 | 真实反映开发过程,安全可追溯 | 历史图可能杂乱 |
git rebase | 重写提交历史,形成线性提交序列 | 历史清晰整洁,便于 code review | 改写历史,需遵循黄金法则 |
三种集成方式对比:merge / rebase / cherry-pick
| 方式 | 动作 | 适用场景 | 历史形态 |
|---|---|---|---|
git merge | 创建合并提交,把两个分支历史接合 | 功能分支回主干、长期并行分支合流 | 保留分叉,真实还原协作过程 |
git rebase | 把当前分支的提交逐个"搬"到目标分支顶端 | 私有分支同步主干、保持线性历史 | 线性,无分叉点 |
git cherry-pick | 只复制单个提交到当前分支 | 紧急修复移植、挑选特定功能 | 目标分支顶端新增一个提交 |
三条铁律:公共分支绝不 rebase——重写已发布历史会让协作者的 pull 产生冲突爆炸;cherry-pick 只解决"单点移植",连续 cherry-pick 大段提交不如直接 merge;rebase 前必须保证工作区干净,否则容易把未提交的修改卷进历史重写。
fork 工作流与 Pull Request
GitHub/GitLab 的开源协作模型是 fork 工作流:你把上游仓库 fork 到自己的账号下(得到独立副本),clone 后创建功能分支开发,推送后发起 Pull Request(PR)请求上游合并。上游维护者 review 代码、提出修改意见,CI 自动跑测试,通过后 merge 进主干。这套流程的核心价值是:贡献者不需要上游仓库的任何写权限。与直接共享分支相比,fork + PR 让所有改动都经过审查门槛,是团队协作与开源贡献的标准姿势。
Git 钩子(Hooks)
钩子是 Git 在特定事件前后自动执行的脚本,存放在 .git/hooks/(默认都是 .sample 模板)。常用钩子:pre-commit(提交前检查代码质量)、commit-msg(校验提交信息格式)、pre-push(推送前跑测试)、post-merge(合并后自动装依赖)。钩子默认不随仓库分发——需要用 git config core.hooksPath .githooks 指向版本库内的目录,或借助 husky(npm)、pre-commit(Python)等工具管理,让团队共享同一套钩子。
submodule 与 subtree
在一个仓库里引用另一个仓库是常见需求(共享库、协议定义、主题模板)。git submodule 把子仓库作为独立 gitlink 记录在主仓库,子仓库自己维护 .git,更新需手动 git submodule update,缺点是操作繁琐、容易"孤儿化"(子仓库 URL 失效后无法恢复)。git subtree 则把子仓库的代码直接并入主仓库目录,历史与文件完全融合,可用 git subtree push 回写上游。选型标准:子仓库被多个仓库依赖且独立演进 → submodule;只是固定版本地引用他人代码 → subtree。
示例代码
1. 初始化与首次提交
# 创建新仓库
mkdir my-project && cd my-project
git init
# 创建文件并提交
echo "# My Project" > README.md
git status
# 输出: Untracked files: README.md
git add README.md
git status
# 输出: Changes to be committed: new file: README.md
git commit -m "chore: init project with README"
# 输出: [main (root-commit) a1b2c3d] chore: init project with README
# 查看提交历史
git log --oneline
# 输出: a1b2c3d chore: init project with README
2. 分支开发与合并
# 创建并切换到 feature 分支
git switch -c feature/login
# 在新分支上工作
echo "login: implemented" > login.py
git add login.py && git commit -m "feat: add login module"
# 切回 main 查看差异
git switch main
git log --oneline
# 输出: a1b2c3d chore: init project with README
# 合并 feature 分支
git merge feature/login
# 输出: Merge made by the 'ort' strategy.
# 删除已合并的分支
git branch -d feature/login
3. 处理合并冲突
# 模拟冲突:两个分支修改同一文件的同一行
git switch -c branch-a
echo "line from A" > conflict.txt
git add conflict.txt && git commit -m "branch-a: add file"
git switch main
git switch -c branch-b
echo "line from B" > conflict.txt
git add conflict.txt && git commit -m "branch-b: add file"
git switch main
git merge branch-a
# 成功,无冲突
git merge branch-b
# 输出: CONFLICT (add/add): Merge conflict in conflict.txt
# 查看冲突文件内容
cat conflict.txt
# 输出:
# <<<<<<< HEAD
# line from A
# =======
# line from B
# >>>>>>> branch-b
# 手动编辑解决冲突后
git add conflict.txt
git commit -m "fix: resolve merge conflict in conflict.txt"
4. 远程仓库协作
# 克隆远程仓库
git clone https://github.com/user/project.git
cd project
# 查看远程配置
git remote -v
# 输出: origin https://github.com/user/project.git (fetch)
# origin https://github.com/user/project.git (push)
# 创建功能分支并推送
git switch -c feat/dashboard
echo "dashboard: WIP" > dashboard.py
git add dashboard.py && git commit -m "feat: add dashboard skeleton"
git push origin feat/dashboard
# 输出: * [new branch] feat/dashboard -> feat/dashboard
# 同步远程更新
git fetch origin # 只拉取不合并
git pull origin main # 拉取并合并(fetch + merge 的简写)
5. 救命命令
# stash — 暂存当前未提交的修改
git stash # 保存当前工作
git stash list # 列出所有 stash
git stash pop # 恢复最近的 stash 并删除
git stash apply stash@{2} # 恢复指定 stash 但不删除
# cherry-pick — 挑选特定提交到当前分支
git cherry-pick a1b2c3d # 将 a1b2c3d 的改动应用到当前分支
# reflog — 找回"丢失"的提交
git reflog # 查看 HEAD 的所有移动历史
# 输出: a1b2c3d HEAD@{0}: checkout: moving from feature to main
# e5f6g7h HEAD@{1}: commit: important work
git reset --hard HEAD@{1} # 恢复到之前的状态
# tag — 给历史打标签
git tag v1.0.0 # 轻量标签
git tag -a v1.0.0 -m "v1.0.0 release" # 附注标签
git push origin v1.0.0 # 推送标签到远程
# revert — 撤销某个提交(生成一个"反向提交",不改写历史)
git revert a1b2c3d # 撤销 a1b2c3d 引入的改动
# 输出: [main abc1234] Revert "feat: add login module"
# revert 安全用于公共分支——它保留完整历史,只是新增一个抵消提交
| 操作 | 效果 | 是否改写历史 | 适用场景 |
|---|---|---|---|
git reset --soft HEAD~1 | 撤销提交,修改保留在暂存区 | 是(本地) | 修正最近一次提交的内容或消息 |
git reset --mixed HEAD~1 | 撤销提交,修改回到工作区 | 是(本地) | 重新选择要提交的文件 |
git reset --hard HEAD~1 | 彻底丢弃提交和所有修改 | 是(本地) | 完全放弃最近的改动(慎用!) |
git revert <hash> | 生成一个新的"反向提交"来抵消目标提交 | 否(新增提交) | 撤销已推送到公共分支的提交 |
选择原则:已推送到远程的提交用 revert(安全,不破坏他人工作);仅本地未推送的可以用 reset(干净利落)。
6. rebase 冲突解决完整流程
# 场景:feature 分支落后于 main,需要变基
git switch feature/login
git rebase main
# 输出: CONFLICT (content): Merge conflict in login.py
# 1. 查看冲突状态
git status
# 输出: both modified: login.py
# 2. 查看冲突内容
git diff
# 3. 手动编辑 login.py,删除冲突标记:
# <<<<<<< HEAD / ======= / >>>>>>> feature/login
# 4. 标记已解决并继续
git add login.py
git rebase --continue
# 会打开编辑器让你写本次变基提交的信息(默认沿用原信息)
# 5. 若中途想放弃整个变基
git rebase --abort
# 6. 变基后本地提交哈希全变了,推送需要强制
git push --force-with-lease origin feature/login
# --force-with-lease 比 --force 安全:若远程有他人新提交则拒绝
7. 远程协作:PR 完整流程
# 1. fork 上游仓库后克隆自己的副本
git clone https://github.com/yourname/project.git
cd project
# 2. 添加上游为第二个远程
git remote add upstream https://github.com/original/project.git
git remote -v
# origin 你的 fork(可写)
# upstream 上游(只读)
# 3. 保持主干同步上游
git fetch upstream
git switch main
git merge upstream/main
# 4. 开功能分支开发
git switch -c feat/fix-login-bug
# ... 修改代码 ...
git commit -m "fix: correct session timeout check"
# 5. 推送并创建 PR
git push origin feat/fix-login-bug
# 在 GitHub 网页上点 "Compare & pull request"
# PR 描述模板:做了什么 / 为什么 / 如何测试 / 相关截图
# 6. 收到 review 意见后更新同一分支(追加提交即可,不要 rebase 已推送的提交)
git commit -am "fix: address review feedback"
git push
# 7. 上游合并后清理
git switch main
git pull upstream main
git branch -d feat/fix-login-bug
git push origin --delete feat/fix-login-bug
8. Git 钩子实战:pre-commit 检查
# .githooks/pre-commit — 禁止密钥和调试代码进仓库
#!/bin/bash
set -euo pipefail
# 检查暂存内容是否含疑似私钥
if git diff --cached | grep -E \
'BEGIN (RSA|OPENSSH|EC) PRIVATE KEY|AKIA[0-9A-Z]{16}'; then
echo "错误: 检测到疑似私钥/密钥,拒绝提交"
exit 1
fi
# 检查是否误提交了调试打印
if git diff --cached | grep -E \
'^\+\s*(print|console\.log|pdb\.set_trace|debugger)'; then
echo "错误: 暂存内容包含调试代码"
exit 1
fi
echo "pre-commit 检查通过"
exit 0
# 启用钩子目录(随仓库分发)
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit
# 加急提交时临时跳过(慎用)
git commit --no-verify -m "hotfix: ..."
# npm 项目常用 husky 管理钩子
# npx husky add .husky/pre-commit "npm test"
9. reflog 救援实战
# 场景一:误删分支
git branch -D feature/login # 手滑删了未合并的分支
git reflog | grep "feature/login"
# 输出: abc1234 HEAD@{12}: checkout: moving from feature/login to main
git branch feature/login abc1234 # 用哈希把分支救回来
# 场景二:reset 回退后找回提交
git reset --hard HEAD~3
git reflog
# 输出: def5678 HEAD@{1}: reset: moving to HEAD~3 ← 回退前的位置
git reset --hard def5678
# 场景三:rebase 搞砸后回到变基前
git rebase main
# ... 变基后发现一团糟 ...
git reflog | head -5
git reset --hard HEAD@{4} # 回到变基前的位置
# reflog 默认保留 90 天,GC 前都能救;
# 被 GC 掉的对象可用 git fsck --lost-found 碰碰运气
10. Git 配置最佳实践
# 常用别名(效率神器)
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.last "log -1 --stat"
git config --global alias.unstage "reset HEAD --"
git config --global alias.amend "commit --amend --no-edit"
# 换行符:Linux 提交统一 LF,Windows 检出转 CRLF
git config --global core.autocrlf input
# 提交信息模板
git config --global commit.template ~/.gitmessage
printf '# 类型(范围): 描述\n# feat|fix|docs|style|refactor|test|chore\n' \
> ~/.gitmessage
# .gitignore 要点:密钥与环境变量必须忽略
# .env
# .env.*
# *.pem
# *.key
# !.env.example # 模板文件保留给团队参考
# 查看配置层级与来源
git config --list --show-origin
常见错误
| 错误表现 | 根因 | 正确做法 |
|---|---|---|
fatal: Not a git repository | 当前目录不是 Git 仓库(没有 .git 文件夹) | 先执行 git init 或 git clone |
| commit 后发现漏了文件 | 忘了 git add 就提交了 | git add forgotten.file && git commit --amend --no-edit |
| merge 冲突后不知所措 | 手动解决后忘了 git add 标记已解决 | 编辑文件→git add→git commit,或 git merge --abort 取消合并 |
git push 被拒绝(non-fast-forward) | 远程分支有本地没有的新提交 | 先 git pull --rebase 或 git fetch + git merge 后再推送 |
git reset --hard 后想找回代码 | 误用了硬重置,丢失了未提交的修改 | 立即检查 git reflog 找回,或使用 IDE 的本地历史 |
最佳实践
| 实践 | 原理 | 示例 |
|---|---|---|
| 小而原子的提交 | 每个提交只做一件事,便于回滚和 code review | git commit -m "fix: correct off-by-one error in pagination" 而非 "fix bugs" |
| 规范的 commit message | 约定式提交(Conventional Commits)让历史可读且可自动化 | type(scope): description — feat: / fix: / chore: / refactor: |
| 功能分支工作流 | 主干分支保持可发布状态,新功能在独立分支开发 | main 为稳定分支,feat/xxx 为功能分支 |
推送前先 pull --rebase | 避免产生多余的 merge commit,保持历史整洁 | git pull --rebase origin main |
| 绝不 force push 到公共分支 | 强制推送会覆盖他人提交,造成协作混乱 | 只在个人分支或紧急修复时用 git push --force-with-lease |
| 及时删除已合并的分支 | 减少分支噪声,避免误操作 | git branch -d feature/done(删除本地);git push origin --delete feature/done(删除远程) |
练习题
- (概念)Git 的三个区域分别是什么?
git add和git commit各自完成什么区域之间的数据移动? - (概念)
git merge和git rebase的根本区别是什么?什么情况下应该使用 rebase 而不是 merge? - (实操)初始化一个 Git 仓库,创建
index.html和style.css两个文件,分两次提交(先提交 index.html,再提交 style.css)。然后创建一个feature/redesign分支,修改两个文件并提交。切回main分支,将 feature 分支合并进来。 - (实操)模拟一个合并冲突场景:在两个分支上修改同一个文件的同一行,手动解决冲突,最后提交解决结果。记录完整的命令序列。
- (🔍 挑战)模拟误操作场景:在 main 分支上执行
git reset --hard HEAD~2丢失了两个最新提交。用git reflog找回丢失的提交。然后练习git cherry-pick将其中一个提交应用到另一个分支上。
点击查看答案
- (概念)三个区域:工作区(Working Directory)、暂存区(Staging Area/Index)、仓库(Repository/.git)。
git add将工作区的修改移入暂存区,git commit将暂存区的内容移入仓库形成一个新的提交。 - (概念)
git merge创建一个新的合并提交(merge commit),保留所有分支的历史完整;git rebase将当前分支的提交"移植"到目标分支之上,形成线性历史。在私有分支尚未推送到远程时,适合用 rebase 保持历史整洁;公共分支上绝对不要 rebase。 - (实操)命令序列:
git init→ 创建 index.html →git add index.html && git commit -m "add index"→ 创建 style.css →git add style.css && git commit -m "add style"→git switch -c feature/redesign→ 修改两个文件 →git add -A && git commit -m "redesign"→git switch main→git merge feature/redesign。注意git log --oneline --graph可验证分支合并后的拓扑。 - (实操)步骤:① main 分支上修改 README.md 第一行并提交;② 创建并切换到 hotfix 分支;③ hotfix 上修改同一文件同一行并提交;④ 切回 main,
git merge hotfix会报告冲突;⑤ 手动编辑文件解决冲突→删除冲突标记→git add README.md && git commit。git mergetool可用图形化工具辅助解决。 - (🔍 挑战)步骤:①
git reset --hard HEAD~2丢失提交;②git reflog找到丢失提交的 hash;③git reset --hard <hash>恢复;④ 切换到另一个分支,git cherry-pick <hash>应用单个提交。reflog默认保留 90 天内的 HEAD 移动记录——即使reset或rebase操作也能找回。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释 Git 三区域模型(工作区、暂存区、仓库) | 尝试向他人讲解 |
| 命令操作 | 能不查文档完成 add、commit、push、pull、branch 的日常操作 | 在终端实际执行 |
| 原理掌握 | 能说出 merge 和 rebase 的区别以及各自的适用场景 | 画出分支合并图 |
| 故障排查 | 能独立解决合并冲突和误操作后的恢复(reflog) | 模拟冲突并解决 |
| 最佳实践 | 能说明为什么公共分支不应该使用 rebase 重写历史 | 对比不同协作方式 |
本章总结
Git 是现代开发最基础的协作工具。三区域模型(工作区→暂存区→仓库)是其核心设计,理解它就能理解 add、commit、status、diff 等命令的行为。分支是 Git 的杀手锏——低成本创建和切换让并行开发变得简单。合并冲突不是灾难,而是协作的正常组成部分。掌握 stash、reflog、cherry-pick 等救命命令,让你在出错时总能找到退路。
速查表
| 命令 | 用途 |
|---|---|
git init | 初始化新仓库 |
git clone <url> | 克隆远程仓库 |
git status | 查看当前状态 |
git add <file> | 添加文件到暂存区 |
git commit -m "msg" | 提交暂存区到仓库 |
git log --oneline --graph | 查看提交历史(简洁图形) |
git diff | 查看工作区与暂存区的差异 |
git diff --cached | 查看暂存区与仓库的差异 |
git branch <name> | 创建分支 |
git switch -c <name> | 创建并切换到新分支 |
git merge <branch> | 合并指定分支到当前分支 |
git pull --rebase | 拉取远程更新并以 rebase 方式合并 |
git stash | 暂存工作区修改 |
git reflog | 查看 HEAD 移动历史(救命神器) |
git reset --soft HEAD~1 | 撤销上次提交但保留修改 |
git cherry-pick <hash> | 将指定提交应用到当前分支 |
学习路径建议
- 学完本章后继续学习 4.18:Git 进阶 Git 进阶(子模块、bisect、高级 rebase 等)
- 结合 4.9:CI/CD 持续部署 CI/CD 理解 Git 在自动化流水线中的角色
- 深入可读 6.7:GitOps 入门 GitOps 入门