跳转至

高级技巧

🎯 学习目标

  • 掌握 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 找回。

Bash
# 查看 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 用二分法帮你高效定位。

Bash
# 开始二分查找
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(推荐):

Bash
# 用脚本自动判断好坏
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(追溯每一行代码的作者)

Bash
# 查看文件每一行的最后修改者
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(查看每次提交的详细改动)

Bash
# 查看某文件的完整修改历史
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(子模块)

用于在一个仓库中引用另一个仓库。

Bash
# 添加子模块
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 文件:

INI
[submodule "libs/lib"]
    path = libs/lib
    url = https://github.com/user/lib.git
    branch = main

4.2 git subtree(子树)

更简单的方式引入外部仓库,不需要特殊的初始化步骤。

Bash
# 添加子树
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 可以让你在同一个仓库中同时检出多个分支到不同目录,无需反复切换分支。

Bash
# 创建新的工作区
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 示例

Bash
# .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"
Bash
# .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 项目)

Bash
# 安装
npm install -D husky lint-staged

# 初始化 husky
npx husky init
Text Only
// package.json
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss}": [
      "prettier --write"
    ],
    "*.md": [
      "prettier --write"
    ]
  }
}
Bash
# .husky/pre-commit
npx lint-staged

# .husky/commit-msg
npx commitlint --edit $1

commitlint 配置:

JavaScript
// 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 项目)

Bash
# 安装
pip install pre-commit

# 创建配置文件
pre-commit sample-config > .pre-commit-config.yaml
YAML
# .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
Bash
# 安装 hooks
pre-commit install

# 手动运行所有检查
pre-commit run --all-files

# 更新 hooks 版本
pre-commit autoupdate

7. git rebase -i(交互式变基)

交互式变基是整理提交历史的强大工具。

Bash
# 修改最近 3 个提交
git rebase -i HEAD~3

执行后会打开编辑器:

Text Only
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(压缩多个提交):

Text Only
pick a1b2c3d feat: add login page
squash f4e5d6c fix: login button style
squash g7h8i9j feat: add login validation
# 三个提交压缩为一个

Fixup(压缩并丢弃提交信息):

Text Only
pick a1b2c3d feat: add login page
fixup f4e5d6c fix: typo
fixup g7h8i9j fix: another typo
# 只保留第一个提交的信息

Reword(修改提交信息):

Text Only
reword a1b2c3d feat: add login page  # 会打开编辑器让你修改
pick f4e5d6c fix: login style

Edit(修改提交内容):

Text Only
edit a1b2c3d feat: add login page    # Git 会在这里停下
pick f4e5d6c fix: login style
Bash
# Git 停在 a1b2c3d 后,你可以:
# 修改文件...
git add .
git commit --amend
git rebase --continue  # 继续变基

调整提交顺序:

Text Only
pick g7h8i9j feat: add login validation  # 移到前面
pick a1b2c3d feat: add login page
pick f4e5d6c fix: login button style

⚠️ 注意:交互式变基会改写历史!只在本地分支上使用,不要对已推送的提交使用(除非你是唯一开发者)。

实用技巧 —— --autosquash

Bash
# 提交时使用 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)用于管理大文件(视频、模型、数据集等)。

Bash
# 安装 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 签名可以证明提交是由你本人做出的,防止身份伪造。

Bash
# 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(单一仓库)是将多个项目/包放在一个仓库中管理。

目录结构示例:

Text Only
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 技巧:

Bash
# 稀疏检出(只检出部分目录)
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 如何处理特定文件。

Text Only
# 强制使用 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 仓库语言比例):

Text Only
# 排除某些文件不计入语言统计
*.min.js linguist-vendored
*.bundle.js linguist-generated
docs/**/*.html linguist-documentation

✏️ 练习题

基础练习

  1. reflog 实战:
  2. 创建 3 个提交
  3. 使用 git reset --hard HEAD~3 回退
  4. 通过 git reflog 找回所有提交

  5. bisect 实战:

  6. 创建 10 次提交,在第 5 次有意引入一个 bug
  7. 使用 git bisect 定位出错的提交

  8. 交互式变基:

  9. 创建 5 次琐碎提交(如"fix typo"、"update"等)
  10. 使用 git rebase -i 压缩为 1-2 个有意义的提交

进阶练习

  1. Git Hooks 实战:
  2. 编写 pre-commit hook 检查 Python 文件是否有 print() 语句
  3. 编写 commit-msg hook 验证提交信息格式

  4. Git LFS 实战:

  5. 配置 Git LFS 追踪 .zip 文件
  6. 添加、提交、推送一个大文件
  7. 查看 .gitattributes 的变化

  8. 使用 git worktree 同时在两个分支上工作:

  9. 创建主工作区在 main 分支
  10. 创建第二个工作区在 feature 分支
  11. 同时在两个目录中开发

📋 面试要点

  • 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

← 上一章:远程协作 | 下一章:Git实战与面试 →