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)——将一个分支的提交移植到另一个分支顶端,保持历史线性

知识关联

原理讲解

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 statusgit 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 initgit clone
commit 后发现漏了文件忘了 git add 就提交了git add forgotten.file && git commit --amend --no-edit
merge 冲突后不知所措手动解决后忘了 git add 标记已解决编辑文件→git addgit commit,或 git merge --abort 取消合并
git push 被拒绝(non-fast-forward)远程分支有本地没有的新提交git pull --rebasegit fetch + git merge 后再推送
git reset --hard 后想找回代码误用了硬重置,丢失了未提交的修改立即检查 git reflog 找回,或使用 IDE 的本地历史

最佳实践

实践原理示例
小而原子的提交每个提交只做一件事,便于回滚和 code reviewgit commit -m "fix: correct off-by-one error in pagination" 而非 "fix bugs"
规范的 commit message约定式提交(Conventional Commits)让历史可读且可自动化type(scope): descriptionfeat: / 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(删除远程)

练习题

  1. (概念)Git 的三个区域分别是什么?git addgit commit 各自完成什么区域之间的数据移动?
  2. (概念)git mergegit rebase 的根本区别是什么?什么情况下应该使用 rebase 而不是 merge?
  3. (实操)初始化一个 Git 仓库,创建 index.htmlstyle.css 两个文件,分两次提交(先提交 index.html,再提交 style.css)。然后创建一个 feature/redesign 分支,修改两个文件并提交。切回 main 分支,将 feature 分支合并进来。
  4. (实操)模拟一个合并冲突场景:在两个分支上修改同一个文件的同一行,手动解决冲突,最后提交解决结果。记录完整的命令序列。
  5. (🔍 挑战)模拟误操作场景:在 main 分支上执行 git reset --hard HEAD~2 丢失了两个最新提交。用 git reflog 找回丢失的提交。然后练习 git cherry-pick 将其中一个提交应用到另一个分支上。
点击查看答案
  1. (概念)三个区域:工作区(Working Directory)、暂存区(Staging Area/Index)、仓库(Repository/.git)。git add 将工作区的修改移入暂存区,git commit 将暂存区的内容移入仓库形成一个新的提交。
  2. (概念)git merge 创建一个新的合并提交(merge commit),保留所有分支的历史完整;git rebase 将当前分支的提交"移植"到目标分支之上,形成线性历史。在私有分支尚未推送到远程时,适合用 rebase 保持历史整洁;公共分支上绝对不要 rebase。
  3. (实操)命令序列: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 maingit merge feature/redesign。注意 git log --oneline --graph 可验证分支合并后的拓扑。
  4. (实操)步骤:① main 分支上修改 README.md 第一行并提交;② 创建并切换到 hotfix 分支;③ hotfix 上修改同一文件同一行并提交;④ 切回 main,git merge hotfix 会报告冲突;⑤ 手动编辑文件解决冲突→删除冲突标记→git add README.md && git commitgit mergetool 可用图形化工具辅助解决。
  5. (🔍 挑战)步骤:① git reset --hard HEAD~2 丢失提交;② git reflog 找到丢失提交的 hash;③ git reset --hard <hash> 恢复;④ 切换到另一个分支,git cherry-pick <hash> 应用单个提交。reflog 默认保留 90 天内的 HEAD 移动记录——即使 resetrebase 操作也能找回。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释 Git 三区域模型(工作区、暂存区、仓库)尝试向他人讲解
命令操作能不查文档完成 add、commit、push、pull、branch 的日常操作在终端实际执行
原理掌握能说出 merge 和 rebase 的区别以及各自的适用场景画出分支合并图
故障排查能独立解决合并冲突和误操作后的恢复(reflog)模拟冲突并解决
最佳实践能说明为什么公共分支不应该使用 rebase 重写历史对比不同协作方式

本章总结

Git 是现代开发最基础的协作工具。三区域模型(工作区→暂存区→仓库)是其核心设计,理解它就能理解 addcommitstatusdiff 等命令的行为。分支是 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>将指定提交应用到当前分支

学习路径建议

延伸阅读

常见问题

提交信息写错了怎么改?
如果还没推送,用 git commit --amend 修改最近一次提交信息。如果已经推送,用 git commit --amend 后强制推送 git push --force-with-lease(注意安全,不要用 --force)。已推送的历史建议不要再改,会影响协作者。
merge 冲突怎么解决?
冲突标记 <<<<<<< HEAD 到 ======= 是当前分支的版本,======= 到 >>>>>>> branch 是合并进来的版本。手动编辑文件保留正确内容,删除冲突标记,然后 git add file 标记已解决,最后 git commit 完成合并。推荐用 git mergetool 启动图形化合并工具。
不小心 reset 丢了提交怎么办?
使用 git reflog 查看 HEAD 移动历史,找到丢失提交的哈希值,用 git reset --hard HASH 恢复。git reflog 记录了所有 HEAD 变化,只要在本地执行过,就能找回。注意 reflog 只记录本地操作,远程的分支操作不会出现在本地 reflog 中。
↑ 回到顶部