- 引言
- 1. 理解 Git 的内部结构
- 2. 必备命令速查表
- 3. 分支策略对比
- 4. Merge vs Rebase
- 5. 精通高级命令
- 6. 优化 .gitconfig
- 7. GitHub PR 评审最佳实践
- 8. 提交信息规范
- 9. Monorepo 管理
- 10. 危机情况的应对方法
- 收尾
引言
开发者每天都在用 Git,但大多数人始终没能走出 add-commit-push 的循环。一遇到 merge 冲突就慌,对 rebase 心存畏惧,甚至根本不知道 bisect 和 worktree 的存在。
本文从 Git 的内部结构讲起,涵盖实务中必须掌握的高级命令、分支策略、PR 评审文化、Monorepo 管理,直到危机情况的应对方法,用 350 行以上的篇幅总结Git 的一切。
1. 理解 Git 的内部结构
想把 Git 用好,就得理解它的内部结构。Git 本质上是一个键值存储(content-addressable filesystem)。
1.1 四种核心对象
Git 的所有数据都以四种对象类型保存。
| 对象 | 作用 | 说明 |
|---|---|---|
| blob | 文件内容 | 文件的快照(不含名字,只有内容) |
| tree | 目录 | 引用 blob 和其他 tree 的列表 |
| commit | 快照 | tree + 父提交 + 元数据 |
| tag | 标签 | 打在特定提交上的 annotated tag |
每个对象都由 SHA-1 哈希标识。内容相同就会得到相同的哈希,所以 Git 天然地做到了去重。
# 确认对象类型
git cat-file -t HEAD
# 查看提交对象的内容
git cat-file -p HEAD
# 探索 tree 对象
git ls-tree HEAD
1.2 三个区域
Git 中有三个核心区域。
- Working Directory -- 实际文件所在的目录。是我们编辑的空间。
- Staging Area (Index) -- 准备纳入下一次提交的变更所在的区域。
- Repository (.git) -- 保存提交历史与所有对象的空间。
# 三个区域之间的文件流转
# Working -> Staging
git add file.txt
# Staging -> Repository
git commit -m "feat: add file"
# Repository -> Working(还原特定文件)
git checkout HEAD -- file.txt
1.3 HEAD 与 ref
- HEAD -- 指向当前检出提交的指针。通常指向某个分支。
- branch -- 指向特定提交的可移动指针。
- tag -- 指向特定提交的固定指针。
# 确认 HEAD 指向哪里
cat .git/HEAD
# ref: refs/heads/main
# 确认分支指向的提交
cat .git/refs/heads/main
# 制造 Detached HEAD 状态
git checkout HEAD~2
2. 必备命令速查表
把每天都要用的命令按场景整理了一遍。
2.1 基本工作流
# 初始化 / 克隆仓库
git init
git clone https://github.com/user/repo.git
# 确认变更
git status
git diff # unstaged 变更
git diff --staged # staged 变更
git diff HEAD # 全部变更
# 暂存与提交
git add -p # 交互式部分暂存
git commit -m "feat: login" # 提交
git commit --amend # 修改最后一次提交
# 与远程同步
git fetch origin # 只取回远程变更
git pull --rebase origin main # fetch + rebase
git push origin feature/login # 推送
2.2 分支管理
# 创建分支并切换
git branch feature/auth
git checkout -b feature/auth # 创建 + 切换
git switch -c feature/auth # Git 2.23+ 推荐
# 分支列表
git branch -a # 本地 + 远程
git branch --merged # 已合并的分支
git branch -d feature/old # 安全删除
git branch -D feature/old # 强制删除
# 清理远程分支
git remote prune origin
git fetch -p
2.3 stash -- 临时保存工作
工作到一半却要紧急切到别的分支时,就用 stash。
# 保存当前变更
git stash
git stash push -m "WIP: login form"
# 查看 stash 列表
git stash list
# 恢复
git stash pop # 取出的同时删除
git stash apply stash@{0} # 取出但保留
# 只 stash 特定文件
git stash push -m "partial" -- src/auth.ts
# 把 stash 转成分支
git stash branch new-branch stash@{0}
3. 分支策略对比
3.1 Git Flow
最传统的分支模型,由 Vincent Driessen 在 2010 年提出。
结构:
main-- 生产代码develop-- 下一个版本的开发feature/*-- 功能开发release/*-- 发布准备hotfix/*-- 紧急缺陷修复
适合的团队:发布周期较长的项目(每月 1-2 次),需要同时维护多个版本的场景。
缺点:分支多导致复杂,与 CI/CD 配合不佳。
3.2 GitHub Flow
GitHub 自己实际使用的简单模型。
结构:
main-- 始终处于可部署状态feature/*-- 从 main 分出,通过 PR 合并
适合的团队:持续部署环境、小规模团队、Web 服务。
规则:main 必须始终可部署,在 feature 分支上作业后通过 PR 合并。
3.3 Trunk-Based Development
Google、Meta 等大型 IT 企业采用的策略。
结构:
main(trunk) -- 所有开发者直接提交,或使用 short-lived 分支作业- 分支寿命:最多 1-2 天
适合的团队:具备 Feature Flag 基础设施的团队、大规模组织、需要快速反馈循环的场景。
3.4 对比小结
| 项目 | Git Flow | GitHub Flow | Trunk-Based |
|---|---|---|---|
| 复杂度 | 高 | 低 | 中 |
| 部署频率 | 每月 1-2 次 | 每天 | 每天数次 |
| 团队规模 | 大规模 | 中小规模 | 任意规模 |
| 是否需要 Feature Flag | 否 | 否 | 是 |
| 与 CI/CD 的契合度 | 一般 | 好 | 非常好 |
4. Merge vs Rebase
这是 Git 中最有争议的话题之一。
4.1 Merge -- 保留历史
# 把 feature 分支 merge 进 main
git checkout main
git merge feature/auth
merge 会找到两个分支的共同祖先并执行 3-way merge,生成一个合并提交(merge commit)。历史被原样保留,因此可以追溯"什么时候在哪个分支上做了什么"。
4.2 Rebase -- 干净的历史
# 在 feature 分支上取得 main 的最新变更
git checkout feature/auth
git rebase main
rebase 会把 feature 分支的提交逐个重新应用到 main 的末端。结果是形成线性历史。
4.3 Interactive Rebase -- 整理提交的核心
# 整理最近 5 个提交
git rebase -i HEAD~5
在交互式变基的编辑器里可以使用的命令如下。
- pick -- 保留提交
- reword -- 修改提交信息
- edit -- 修改提交内容
- squash -- 与前一个提交合并(合并信息)
- fixup -- 与前一个提交合并(丢弃信息)
- drop -- 删除提交
# 实际用例:提 PR 之前整理提交
# 在编辑器中改成下面这样
# pick abc1234 feat: add login page
# squash def5678 fix: typo in login
# squash ghi9012 style: adjust padding
# -> 3 个提交被合并成 1 个
4.4 什么时候用哪一个
| 场景 | 推荐 |
|---|---|
| PR 合并 | Squash Merge 或 Rebase Merge |
| 整理本地提交 | Interactive Rebase |
| 共享分支 | Merge(禁止 rebase) |
| 个人 feature 分支 | 用 Rebase 同步 main |
黄金法则:已经 push 出去的提交不要 rebase。别人可能正在引用它。
5. 精通高级命令
5.1 cherry-pick -- 只取特定提交
把其他分支上的某一个提交复制到当前分支。
# 取得特定提交
git cherry-pick abc1234
# 一次取多个提交
git cherry-pick abc1234 def5678
# 按范围取(不含 abc,含 def)
git cherry-pick abc1234..def5678
# 不提交,只应用变更
git cherry-pick --no-commit abc1234
使用场景:
- 把 hotfix 分支的缺陷修复也应用到 develop
- 把提交错分支的内容挪到正确的分支
- 在发布分支上只挑选特定功能纳入
5.2 bisect -- 用二分查找定位缺陷
在几百个提交中用二分查找定位缺陷是从哪里引入的。
# 开始 bisect
git bisect start
# 把当前(有缺陷)标记为 bad
git bisect bad
# 把当时正常的提交标记为 good
git bisect good v2.0.0
# Git 会检出中间的提交 -> 测试 -> 判定
git bisect good # 或者 git bisect bad
# 找到问题提交后重置
git bisect reset
也可以自动化。
# 用测试脚本自动 bisect
git bisect start HEAD v2.0.0
git bisect run npm test
这样一来,Git 会自动在每个提交上跑测试,并找出第一个失败的提交。
5.3 worktree -- 同时在多个分支上作业
可以从一个仓库创建多个工作目录,同时在不同分支上作业。
# 创建新的 worktree
git worktree add ../project-hotfix hotfix/critical-bug
# 查看 worktree 列表
git worktree list
# 移除 worktree
git worktree remove ../project-hotfix
# 一边创建新分支一边创建 worktree
git worktree add -b feature/new-ui ../project-ui
使用场景:
- 在 main 上做代码评审的同时,在 feature 上继续开发
- 紧急 hotfix 与当前工作同时推进
- 等待 CI 构建的空档里开始别的工作
5.4 reflog -- 失误恢复的最后防线
reflog 是 HEAD 曾经指向过的所有位置的记录。哪怕失手丢掉了提交,也能靠 reflog 找回来。
# 查看 reflog
git reflog
# 误执行 reset --hard 之后的恢复
git reflog
# 找到 HEAD@{2}: commit: important feature 之后
git reset --hard HEAD@{2}
# 恢复已删除的分支
git reflog
git checkout -b recovered-branch HEAD@{5}
# 查看特定时间段内的记录
git reflog --since="2 days ago"
注意:reflog 只存在于本地,且默认 90 天后过期。
6. 优化 .gitconfig
介绍能提升生产力的 Git 配置。
6.1 好用的 alias
[alias]
# 状态与日志
st = status -sb
lg = log --oneline --graph --decorate --all
last = log -1 HEAD --stat
# 分支
co = checkout
sw = switch
br = branch -vv
brd = branch -d
# 提交
cm = commit -m
ca = commit --amend --no-edit
undo = reset HEAD~1 --mixed
# diff
df = diff --stat
dfc = diff --cached
# stash
sl = stash list
sp = stash pop
ss = stash push -m
# 清理
cleanup = "!git branch --merged | grep -v '\\*\\|main\\|develop' | xargs -n 1 git branch -d"
6.2 core 配置
[core]
editor = code --wait
autocrlf = input # macOS/Linux
ignorecase = false
pager = delta # 使用 delta pager
[pull]
rebase = true # pull 时总是 rebase
[push]
default = current # 只 push 当前分支
autoSetupRemote = true # push 时自动设置 upstream
[init]
defaultBranch = main
[diff]
tool = vscode
colorMoved = default
[merge]
conflictstyle = diff3 # 3-way 冲突显示
tool = vscode
[rerere]
enabled = true # 记住冲突的解决方式
6.3 delta -- 更好用的 diff 工具
delta 是把 Git diff 输出变得更易读的工具。
# 安装
brew install git-delta
# 添加到 .gitconfig
[core]
pager = delta
[interactive]
diffFilter = delta --color-only
[delta]
navigate = true
line-numbers = true
side-by-side = true
7. GitHub PR 评审最佳实践
7.1 好 PR 的条件
- 体量:200-400 行以内。超过就拆开。
- 单一目的:一个 PR 只做一件事。
- 说明:写清楚为什么需要这个变更、是怎么测试的。
- Self-Review:提交之前自己先审一遍。
7.2 PR 模板
## 变更内容
- 改了什么、为什么改
## 测试
- [ ] 新增/修改了单元测试
- [ ] 在本地确认过行为
- [ ] 验证过边界情况
## 截图(涉及 UI 变更时)
## 相关 issue
- closes #123
7.3 评审礼仪
评审者:
- 先理解代码的"意图"。比起风格,更该关注逻辑。
- 用提问的方式给反馈:"这里为什么这么写?"比"这是错的"要好。
- 区分 nit、suggestion、question 等评论类型。
- 明确地使用 Approve、Request Changes、Comment。
作者:
- 尊重评审者的时间。把 PR 保持得小一些。
- 回应每一条评论(至少给个表情反应)。
- force push 之后要通知评审者。
7.4 CODEOWNERS
# .github/CODEOWNERS
# 整个代码库
* @team-lead
# 前端
/src/components/ @frontend-team
/src/pages/ @frontend-team
# 后端 API
/src/api/ @backend-team
# 基础设施
/terraform/ @devops-team
/k8s/ @devops-team
# 安全敏感文件
/src/auth/ @security-team @team-lead
8. 提交信息规范
8.1 Conventional Commits
type(scope): description
body(可选)
footer(可选)
type 的种类:
| type | 说明 |
|---|---|
| feat | 新功能 |
| fix | 缺陷修复 |
| docs | 文档变更 |
| style | 代码格式化(不改变功能) |
| refactor | 重构 |
| perf | 性能改进 |
| test | 测试新增/修改 |
| chore | 构建、配置变更 |
| ci | CI 配置变更 |
# 示例
git commit -m "feat(auth): add Google OAuth login"
git commit -m "fix(api): handle null response from payment gateway"
git commit -m "docs(readme): update installation instructions"
# Breaking Change
git commit -m "feat(api)!: change response format to JSON:API"
8.2 gitmoji
用表情符号在视觉上区分提交类型。
git commit -m ":sparkles: add user profile page"
git commit -m ":bug: fix login redirect loop"
git commit -m ":recycle: refactor database connection pool"
git commit -m ":white_check_mark: add unit tests for auth module"
8.3 用 commitlint 强制执行
# 安装
npm install -D @commitlint/cli @commitlint/config-conventional
# commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style',
'refactor', 'perf', 'test', 'chore', 'ci'
]],
'subject-max-length': [2, 'always', 72],
},
};
9. Monorepo 管理
9.1 git sparse-checkout
在大规模 Monorepo 中只检出需要的目录。
# 启用 sparse-checkout
git sparse-checkout init --cone
# 只指定需要的目录
git sparse-checkout set packages/frontend packages/shared
# 追加目录
git sparse-checkout add packages/backend
# 确认配置
git sparse-checkout list
# 恢复全部
git sparse-checkout disable
9.2 git subtree
把外部仓库作为子目录纳入统一管理。
# 添加外部仓库
git subtree add --prefix=libs/shared-utils \
https://github.com/org/shared-utils.git main --squash
# 拉取变更
git subtree pull --prefix=libs/shared-utils \
https://github.com/org/shared-utils.git main --squash
# 推送变更
git subtree push --prefix=libs/shared-utils \
https://github.com/org/shared-utils.git main
9.3 与 Nx / Turborepo 联动
把 Monorepo 构建工具和 Git 联动起来,就能只构建、只测试发生变更的包。
# Nx:只测试受影响的项目
npx nx affected --target=test --base=main --head=HEAD
# Turborepo:只构建发生变更的包
npx turbo run build --filter=...[HEAD~1]
# 在 GitHub Actions 中检测变更
# CI 中与 main 比较,只处理发生变更的包
10. 危机情况的应对方法
10.1 force push 的恢复
这是有人误做了 force push 时的应对方法。
# 1. 在 reflog 中找到原来的提交
git reflog show origin/main
# 2. 恢复到原来的提交
git push origin HEAD@{1}:main --force
# 3. 通知其他团队成员
# "main 分支已恢复。请执行 git pull --rebase"
预防措施:
# 阻止对 main 分支的 force push(GitHub Settings)
# Settings -> Branches -> Branch protection rules
# 启用 "Restrict force pushes"
10.2 移除敏感信息 (BFG Repo-Cleaner)
这是不小心把密码、API 密钥等提交上去时的做法。
# 安装 BFG
brew install bfg
# 从历史中彻底移除特定文件
bfg --delete-files secrets.env
# 替换特定文本
bfg --replace-text passwords.txt
# 清理
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# 强制推送
git push --force
注意事项:
- 执行 BFG 之前务必备份仓库。
- 必须要求已经 clone 过的团队成员重新做一次 fresh clone。
- 可能还需要向 GitHub 申请清除缓存。
10.3 大文件管理 (Git LFS)
二进制文件、媒体文件等大文件要用 Git LFS。
# 安装并初始化 Git LFS
brew install git-lfs
git lfs install
# 指定要追踪的文件模式
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "assets/videos/*"
# .gitattributes 会自动生成 - 需要提交
git add .gitattributes
git commit -m "chore: configure Git LFS tracking"
# 查看 LFS 文件列表
git lfs ls-files
# 把已经提交的大文件迁移到 LFS
git lfs migrate import --include="*.psd" --everything
10.4 撤销错误的 merge
# revert 掉 merge 提交本身
git revert -m 1 MERGE_COMMIT_SHA
# -m 1: 以第一个父提交(main)为基准撤销
# -m 2: 以第二个父提交(feature)为基准撤销
10.5 什么都丢了的时候的最后手段
# 1. 查看 reflog(90 天以内)
git reflog
# 2. 从 dangling 对象中恢复
git fsck --lost-found
# 3. 最坏的情况:从团队成员的本地仓库恢复
# 请团队成员 push,或者用 bundle 拿过来
git bundle create backup.bundle --all
收尾
Git 不只是一个版本管理工具。它定义团队的协作方式、保证代码质量,是部署流水线的根基,属于核心基础设施。
本文讲过的内容可以总结如下。
- 理解内部结构之后,命令就变得直观易懂了。
- 分支策略要按团队的实际情况来选 -- 没有标准答案。
- rebase 用在本地,merge 用在共享分支。
- cherry-pick、bisect、worktree、reflog 会在实战中大放异彩。
- PR 评审文化决定代码质量。
- 用 Conventional Commits 把提交历史保持得干净。
- 在危机情况下,reflog 和 BFG 就是救命绳。
精通 Git 不是一朝一夕的事。但只要每天试一个新命令,一点点理解它的内部结构,不知不觉间你就会成为在任何情况下都不慌不忙的 Git 高手。
현재 단락 (1/305)
开发者每天都在用 Git,但大多数人始终没能走出 add-commit-push 的循环。一遇到 merge 冲突就慌,对 rebase 心存畏惧,甚至根本不知道 bisect 和 worktree ...