高级技巧¶
🎯 学习目标¶
- 掌握 git reflog 找回丢失的提交
- 学会 git bisect 高效定位引入 bug 的提交
- 熟悉 git blame、submodule、worktree 等高级功能
- 配置 Git Hooks 实现自动化工作流
- 掌握交互式变基(rebase -i)整理提交历史
- 了解 Git LFS、GPG 签名、Monorepo 等工程实践
1. git reflog(找回丢失的提交)¶
reflog 记录了 HEAD 的每一次变动,是 Git 的"后悔药"。即使你 reset --hard 或删除了分支,也能通过 reflog 找回。
# 查看 reflog
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~2
# f4e5d6c HEAD@{1}: commit: feat: add payment
# g7h8i9j HEAD@{2}: commit: fix: login bug
# ...
# 找回丢失的提交
git reset --hard f4e5d6c # 恢复到指定的提交
# 或者
git switch -c recovery f4e5d6c # 创建新分支恢复
# 找回删除的分支
git reflog
# 找到该分支最后一次提交的 hash
git switch -c restored-branch <commit-hash>
# reflog 有过期时间(默认 90 天)
git reflog expire --expire=now --all # 手动清理(慎用)
💡 记住:只要 commit 过的内容,在 90 天内基本都可以通过 reflog 找回。所以多 commit,少担心!
2. git bisect(二分查找 Bug)¶
当你知道某个版本是好的、当前版本有 bug,但不知道是哪个提交引入的 bug 时,bisect 用二分法帮你高效定位。
# 开始二分查找
git bisect start
# 标记当前版本有 bug
git bisect bad
# 标记一个已知的正常版本
git bisect good v1.0.0
# Git 会自动切到中间的提交
# 你测试后告诉 Git 这个版本是好是坏
git bisect good # 如果这个版本没问题
git bisect bad # 如果这个版本有问题
# Git 继续二分,直到找到引入 bug 的提交
# 最终输出:
# abc1234 is the first bad commit
# Author: ...
# Date: ...
# commit message...
# 结束 bisect,回到之前的状态
git bisect reset
自动化 bisect(推荐):
# 用脚本自动判断好坏
git bisect start HEAD v1.0.0
git bisect run python test_login.py
# Git 会自动运行测试脚本
# 脚本返回 0 = good,非0 = bad
💡 假设有 1000 个提交,线性查找需要 1000 次测试,bisect 只需约 10 次(\(\log_2{1000} \approx 10\))。
3. git blame / git log -p¶
git blame(追溯每一行代码的作者)¶
# 查看文件每一行的最后修改者
git blame file.py
# a1b2c3d (John 2024-01-15) def login(username):
# f4e5d6c (Alice 2024-01-20) if not username:
# f4e5d6c (Alice 2024-01-20) raise ValueError()
# 查看指定行范围
git blame -L 10,20 file.py
# 忽略空白修改
git blame -w file.py
# 显示原始提交(追溯代码移动/复制)
git blame -C file.py # 跨文件追溯
git blame -CC file.py # 更积极的追溯
git log -p(查看每次提交的详细改动)¶
# 查看某文件的完整修改历史
git log -p -- file.py
# 搜索特定代码的修改历史
git log -S "function_name" # 添加或删除含该字符串的提交
git log -G "regex_pattern" # 正则匹配
# 查看某个函数的修改历史(强大!)
git log -L :function_name:file.py
4. git submodule / git subtree¶
4.1 git submodule(子模块)¶
用于在一个仓库中引用另一个仓库。
# 添加子模块
git submodule add https://github.com/user/lib.git libs/lib
git commit -m "chore: 添加 lib 子模块"
# 克隆包含子模块的仓库
git clone --recursive https://github.com/user/project.git
# 或者
git clone https://github.com/user/project.git
cd project
git submodule init
git submodule update
# 更新子模块到最新
git submodule update --remote
# 删除子模块
git submodule deinit libs/lib
git rm libs/lib
rm -rf .git/modules/libs/lib
git commit -m "chore: 移除 lib 子模块"
.gitmodules 文件:
4.2 git subtree(子树)¶
更简单的方式引入外部仓库,不需要特殊的初始化步骤。
# 添加子树
git subtree add --prefix=libs/lib https://github.com/user/lib.git main --squash
# 拉取子树最新代码
git subtree pull --prefix=libs/lib https://github.com/user/lib.git main --squash
# 向子树推送修改
git subtree push --prefix=libs/lib https://github.com/user/lib.git main
submodule vs subtree 对比:
| 特性 | submodule | subtree |
|---|---|---|
| 复杂度 | 较高 | 较低 |
| 克隆 | 需要 --recursive | 无特殊要求 |
| 存储方式 | 引用(指针) | 完整代码在仓库中 |
| 更新 | submodule update | subtree pull |
| 适用场景 | 大型依赖、频繁更新 | 小型库、不常更新 |
5. git worktree(多工作区)¶
worktree 可以让你在同一个仓库中同时检出多个分支到不同目录,无需反复切换分支。
# 创建新的工作区
git worktree add ../hotfix hotfix/urgent
# 在 ../hotfix 目录检出 hotfix/urgent 分支
# 基于新分支创建工作区
git worktree add -b feature/new ../feature-new
# 查看所有工作区
git worktree list
# /path/to/main abc1234 [main]
# /path/to/hotfix def5678 [hotfix/urgent]
# /path/to/feature-new ghi9012 [feature/new]
# 删除工作区
git worktree remove ../hotfix
# 清理无效的工作区引用
git worktree prune
使用场景:
- 正在开发功能,突然要修 bug?不用 stash,直接在新工作区修
- 同时对比两个分支的运行效果
- 长期维护多个版本分支
6. Git Hooks¶
Git Hooks 是在特定 Git 事件触发时自动运行的脚本,存储在 .git/hooks/ 目录。
6.1 常用 Hooks¶
| Hook | 触发时机 | 常见用途 |
|---|---|---|
pre-commit | commit 之前 | 代码格式检查、Lint |
commit-msg | 编写 commit 信息后 | 验证提交信息格式 |
pre-push | push 之前 | 运行测试 |
post-merge | merge 之后 | 自动安装依赖 |
post-checkout | checkout 之后 | 环境切换提醒 |
6.2 简单 Hook 示例¶
# .git/hooks/pre-commit(需要 chmod +x)
#!/bin/sh
# 检查是否有 console.log
if git diff --cached --name-only | xargs grep -l 'console.log' 2>/dev/null; then
echo "❌ 请移除 console.log 后再提交"
exit 1
fi
echo "✅ Pre-commit check passed"
# .git/hooks/commit-msg
#!/bin/sh
# 验证 Conventional Commits 格式
commit_msg=$(cat "$1") # $()命令替换:执行命令并获取输出
pattern="^(feat|fix|docs|style|refactor|perf|test|chore|ci|revert)(\(.+\))?: .{1,}" # |管道:将前一命令的输出作为后一命令的输入
if ! echo "$commit_msg" | grep -qE "$pattern"; then # grep文本搜索:按模式匹配行
echo "❌ 提交信息不符合 Conventional Commits 格式"
echo "格式:<type>(<scope>): <subject>"
echo "示例:feat(auth): 添加登录功能"
exit 1
fi
6.3 husky + lint-staged(JavaScript/TypeScript 项目)¶
// package.json
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss}": [
"prettier --write"
],
"*.md": [
"prettier --write"
]
}
}
commitlint 配置:
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'subject-max-length': [2, 'always', 72],
'body-max-line-length': [2, 'always', 100],
},
};
6.4 pre-commit 框架(Python 项目)¶
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: ['--maxkb=500']
- repo: https://github.com/psf/black
rev: 24.1.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
# 安装 hooks
pre-commit install
# 手动运行所有检查
pre-commit run --all-files
# 更新 hooks 版本
pre-commit autoupdate
7. git rebase -i(交互式变基)¶
交互式变基是整理提交历史的强大工具。
执行后会打开编辑器:
pick a1b2c3d feat: add login page
pick f4e5d6c fix: login button style
pick g7h8i9j feat: add login validation
# Commands:
# p, pick = 保留该提交
# r, reword = 保留提交但修改提交信息
# e, edit = 保留提交但停下来修改
# s, squash = 合并到前一个提交(保留信息)
# f, fixup = 合并到前一个提交(丢弃信息)
# d, drop = 删除该提交
常见操作¶
Squash(压缩多个提交):
pick a1b2c3d feat: add login page
squash f4e5d6c fix: login button style
squash g7h8i9j feat: add login validation
# 三个提交压缩为一个
Fixup(压缩并丢弃提交信息):
pick a1b2c3d feat: add login page
fixup f4e5d6c fix: typo
fixup g7h8i9j fix: another typo
# 只保留第一个提交的信息
Reword(修改提交信息):
Edit(修改提交内容):
调整提交顺序:
pick g7h8i9j feat: add login validation # 移到前面
pick a1b2c3d feat: add login page
pick f4e5d6c fix: login button style
⚠️ 注意:交互式变基会改写历史!只在本地分支上使用,不要对已推送的提交使用(除非你是唯一开发者)。
实用技巧 —— --autosquash:
# 提交时使用 fixup! 或 squash! 前缀
git commit -m "fixup! feat: add login page"
# 自动整理
git rebase -i --autosquash HEAD~5
# fixup 提交会自动放到对应提交的后面
8. 大文件处理(Git LFS)¶
Git LFS(Large File Storage)用于管理大文件(视频、模型、数据集等)。
# 安装 Git LFS
# macOS: brew install git-lfs
# Ubuntu: sudo apt install git-lfs
# Windows: 下载安装包
# 初始化
git lfs install
# 追踪大文件类型
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "*.h5"
git lfs track "models/**"
# 查看追踪规则(存在 .gitattributes 中)
cat .gitattributes
# *.psd filter=lfs diff=lfs merge=lfs -text
# *.zip filter=lfs diff=lfs merge=lfs -text
# 提交 .gitattributes
git add .gitattributes
git commit -m "chore: 配置 Git LFS"
# 之后正常使用 git add/commit/push 即可
# 大文件会自动通过 LFS 管理
# 查看 LFS 管理的文件
git lfs ls-files
# 获取 LFS 文件
git lfs pull
9. 签名提交(GPG 签名)¶
GPG 签名可以证明提交是由你本人做出的,防止身份伪造。
# 1. 生成 GPG 密钥
gpg --full-generate-key
# 2. 查看密钥
gpg --list-secret-keys --keyid-format=long
# sec rsa4096/ABC123DEF456 2024-01-01 [SC]
# 3. 配置 Git 使用 GPG
git config --global user.signingkey ABC123DEF456
git config --global commit.gpgsign true # 所有提交自动签名
# 4. 签名提交
git commit -S -m "feat: signed commit"
# 5. 验证签名
git log --show-signature
# 6. 导出公钥(添加到 GitHub)
gpg --armor --export ABC123DEF456
在 GitHub 上,签名的提交会显示 Verified 绿色标签。
10. Monorepo 管理策略¶
Monorepo(单一仓库)是将多个项目/包放在一个仓库中管理。
目录结构示例:
monorepo/
├── packages/
│ ├── frontend/
│ │ └── package.json
│ ├── backend/
│ │ └── package.json
│ └── shared/
│ └── package.json
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
常用 Monorepo 工具:
| 工具 | 语言生态 | 特点 |
|---|---|---|
| Turborepo | JavaScript/TypeScript | 增量构建、远程缓存 |
| Nx | JavaScript/TypeScript | 功能丰富、可视化依赖图 |
| Lerna | JavaScript/TypeScript | 经典工具,包版本管理 |
| Bazel | 多语言 | Google 出品,超大项目 |
Git Monorepo 技巧:
# 稀疏检出(只检出部分目录)
git sparse-checkout init --cone
git sparse-checkout set packages/frontend packages/shared
# CODEOWNERS 按目录分配
# .github/CODEOWNERS
/packages/frontend/ @frontend-team
/packages/backend/ @backend-team
/packages/shared/ @core-team
11. .gitattributes 使用¶
.gitattributes 控制 Git 如何处理特定文件。
# 强制使用 LF 换行符(跨平台统一)
* text=auto eol=lf
# 特定文件类型
*.sh text eol=lf
*.bat text eol=crlf
# 二进制文件(不进行 diff/merge)
*.png binary
*.jpg binary
*.pdf binary
# Git LFS
*.psd filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
# 语言统计(GitHub Linguist)
docs/* linguist-documentation
vendor/* linguist-vendored
*.generated.js linguist-generated
# 自定义 diff 驱动
*.md diff=markdown
语言统计控制(GitHub 仓库语言比例):
# 排除某些文件不计入语言统计
*.min.js linguist-vendored
*.bundle.js linguist-generated
docs/**/*.html linguist-documentation
✏️ 练习题¶
基础练习¶
- reflog 实战:
- 创建 3 个提交
- 使用
git reset --hard HEAD~3回退 -
通过
git reflog找回所有提交 -
bisect 实战:
- 创建 10 次提交,在第 5 次有意引入一个 bug
-
使用
git bisect定位出错的提交 -
交互式变基:
- 创建 5 次琐碎提交(如"fix typo"、"update"等)
- 使用
git rebase -i压缩为 1-2 个有意义的提交
进阶练习¶
- Git Hooks 实战:
- 编写
pre-commithook 检查 Python 文件是否有print()语句 -
编写
commit-msghook 验证提交信息格式 -
Git LFS 实战:
- 配置 Git LFS 追踪
.zip文件 - 添加、提交、推送一个大文件
-
查看
.gitattributes的变化 -
使用
git worktree同时在两个分支上工作: - 创建主工作区在 main 分支
- 创建第二个工作区在 feature 分支
- 同时在两个目录中开发
📋 面试要点¶
- git reflog:HEAD 移动的完整日志,90 天内可找回任何 commit
- git bisect:二分查找引入 bug 的提交,时间复杂度 \(O(\log n)\)
- git blame:追溯每一行代码的作者和修改时间
- submodule vs subtree:submodule 是引用(指针),subtree 是完整代码
- Git Hooks:pre-commit(代码检查)、commit-msg(信息格式)、pre-push(运行测试)
- rebase -i:squash/fixup 压缩提交,reword 修改信息,edit 修改内容
- Git LFS:用于管理大文件,文件本体存储在 LFS 服务器
- GPG 签名:证明提交者身份,GitHub 显示 Verified 标签
- Monorepo:一个仓库管理多个项目,需要 sparse-checkout 和 CODEOWNERS