面向实际开发的高级 Git 操作。涵盖交互式变基、二分查找、worktree、reflog 恢复、subtree、submodule、稀疏检出、冲突解决和单体仓库模式。
使用场景
- 合并前清理提交历史(交互式变基)
- 查找哪个提交引入了 Bug(bisect)
- 同时在多个分支上工作(worktree)
- 恢复丢失的提交或撤销错误(reflog)
- 跨仓库管理共享代码(subtree/submodule)
- 解决复杂的合并冲突
- 跨分支或 Fork 挑选提交(cherry-pick)
- 处理大型单体仓库(稀疏检出)
交互式变基
压缩、重排、编辑提交
# 交互式变基最近 5 个提交
git rebase -i HEAD~5# 变基到 main(分叉以来的所有提交)
git rebase -i main
编辑器会打开一个选择列表:
pick a1b2c3d Add user model
pick e4f5g6h Fix typo in user model
pick i7j8k9l Add user controller
pick m0n1o2p Add user routes
pick q3r4s5t Fix import in controller
可用命令:
pick = 原样使用提交
reword = 使用提交但编辑消息
edit = 在此提交后停止以修改
squash = 合并到前一个提交(保留两条消息)
fixup = 合并到前一个提交(丢弃此消息)
drop = 完全删除该提交
常见模式
# 将修复提交压缩到其父提交
# 将修复提交的 "pick" 改为 "fixup":
pick a1b2c3d Add user model
fixup e4f5g6h Fix typo in user model
pick i7j8k9l Add user controller
fixup q3r4s5t Fix import in controller
pick m0n1o2p Add user routes# 重排提交(只需移动行)
pick i7j8k9l Add user controller
pick m0n1o2p Add user routes
pick a1b2c3d Add user model
# 将一个提交拆分为两个
# 标记为 "edit",然后当它停止时:
git reset HEAD~
git add src/model.ts
git commit -m "Add user model"
git add src/controller.ts
git commit -m "Add user controller"
git rebase --continue
自动压缩(自动排列的提交消息)
# 提交修复时,引用要压缩到的提交
git commit --fixup=a1b2c3d -m "Fix typo"
# 或
git commit --squash=a1b2c3d -m "Additional changes"# 稍后,使用自动压缩变基
git rebase -i --autosquash main
# fixup/squash 提交会自动放在其目标之后
中止或继续
git rebase --abort # 取消并恢复原始状态
git rebase --continue # 解决冲突或编辑后继续
git rebase --skip # 跳过当前提交并继续
Bisect(查找 Bug)
通过提交进行二分搜索
# 开始二分查找
git bisect start# 将当前提交标记为坏提交(有 Bug)
git bisect bad
# 标记一个已知的好提交(Bug 出现之前)
git bisect good v1.2.0
# 或:git bisect good abc123
# Git 检出中间提交。测试它,然后:
git bisect good # 如果此提交没有 Bug
git bisect bad # 如果此提交有 Bug
# 重复直到 Git 识别出确切的提交
# "abc123 is the first bad commit"
# 完成 — 返回原始分支
git bisect reset
自动化二分查找(使用测试脚本)
# 完全自动:Git 在每个提交上运行脚本
# 脚本必须以 0 退出表示好,1 退出表示坏
git bisect start HEAD v1.2.0
git bisect run ./test-for-bug.sh# 测试脚本示例
cat > /tmp/test-for-bug.sh << 'EOF'
#!/bin/bash
# 如果 Bug 不存在返回 0,如果 Bug 存在返回 1
npm test -- --grep "login should redirect" 2>/dev/null
EOF
chmod +x /tmp/test-for-bug.sh
git bisect run /tmp/test-for-bug.sh
处理构建失败的二分查找
# 如果某个提交无法编译,跳过它
git bisect skip# 跳过已知损坏的提交范围
git bisect skip v1.3.0..v1.3.5
Worktree(并行分支)
同时在多个分支上工作
# 为不同分支添加工作树
git worktree add ../myproject-hotfix hotfix/urgent-fix
# 创建一个新目录,检出该分支# 添加带新分支的工作树
git worktree add ../myproject-feature -b feature/new-thing
# 列出工作树
git worktree list
# 完成后移除工作树
git worktree remove ../myproject-hotfix
# 清理过期的工作树引用
git worktree prune
使用场景
# 审查 PR 同时保持当前工作不受影响
git worktree add ../review-pr-123 origin/pr-123# 在 feature 分支开发时在 main 上运行测试
git worktree add ../main-tests main
cd ../main-tests && npm test
# 并排比较分支行为
git worktree add ../compare-old release/v1.0
git worktree add ../compare-new release/v2.0
Reflog(恢复)
查看 Git 记住的一切
# 显示 reflog(所有 HEAD 移动)
git reflog
# 输出:
# abc123 HEAD@{0}: commit: Add feature
# def456 HEAD@{1}: rebase: moving to main
# ghi789 HEAD@{2}: checkout: moving from feature to main# 显示特定分支的 reflog
git reflog show feature/my-branch
# 显示带时间戳的 reflog
git reflog --date=relative
从错误中恢复
# 撤销错误的变基(在 reflog 中找到变基前的提交)
git reflog
# 找到:"ghi789 HEAD@{5}: checkout: moving from feature to main"(变基前)
git reset --hard ghi789# 恢复已删除的分支
git reflog
# 找到该分支的最后一个提交
git branch recovered-branch abc123
# 在 reset --hard 后恢复
git reflog
git reset --hard HEAD@{2} # 回退 2 个 reflog 条目
# 恢复丢弃的 stash
git fsck --unreachable | grep commit
# 或
git stash list # 如果还在的话
git log --walk-reflogs --all -- stash # 查找丢弃的 stash 提交
Cherry-Pick
将特定提交复制到另一个分支
# 挑选单个提交
git cherry-pick abc123# 挑选多个提交
git cherry-pick abc123 def456 ghi789
# 挑选范围(排他起始,包含结束)
git cherry-pick abc123..ghi789
# 挑选但不提交(仅暂存更改)
git cherry-pick --no-commit abc123
# 从其他远程/Fork 挑选
git remote add upstream https://github.com/other/repo.git
git fetch upstream
git cherry-pick upstream/main~3 # upstream main 的第 3 个提交
处理 cherry-pick 期间的冲突
# 如果出现冲突:
# 1. 在文件中解决冲突
# 2. 暂存已解决的文件
git add resolved-file.ts
# 3. 继续
git cherry-pick --continue# 或中止
git cherry-pick --abort
Subtree 和 Submodule
Subtree(更简单 — 将代码复制到你的仓库)
# 添加 subtree
git subtree add --prefix=lib/shared https://github.com/org/shared-lib.git main --squash# 从上游拉取更新
git subtree pull --prefix=lib/shared https://github.com/org/shared-lib.git main --squash
# 将本地更改推送回上游
git subtree push --prefix=lib/shared https://github.com/org/shared-lib.git main
# 将 subtree 拆分为独立分支(用于提取)
git subtree split --prefix=lib/shared -b shared-lib-standalone
Submodule(指向特定提交的另一个仓库的指针)
# 添加 submodule
git submodule add https://github.com/org/shared-lib.git lib/shared# 克隆带 submodule 的仓库
git clone --recurse-submodules https://github.com/org/main-repo.git
# 克隆后初始化 submodule(如果忘了 --recurse)
git submodule update --init --recursive
# 将 submodule 更新到最新
git submodule update --remote
# 移除 submodule
git rm lib/shared
rm -rf .git/modules/lib/shared
# 如果 .gitmodules 条目仍然存在则删除
何时使用哪个
Subtree:更简单,克隆者无需特殊命令,代码在你的仓库中。
适用于:共享库、供应商代码、上游变更不频繁。Submodule:指向确切提交,仓库更小,分离清晰。
适用于:大型依赖、独立发布周期、多个贡献者。
稀疏检出(单体仓库)
仅检出你需要的目录
# 启用稀疏检出
git sparse-checkout init --cone# 选择目录
git sparse-checkout set packages/my-app packages/shared-lib
# 添加另一个目录
git sparse-checkout add packages/another-lib
# 列出已检出的内容
git sparse-checkout list
# 禁用(再次检出所有内容)
git sparse-checkout disable
使用稀疏检出克隆(大型单体仓库)
# 部分克隆 + 稀疏检出(对超大仓库最快)
git clone --filter=blob:none --sparse https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set packages/my-service# 无检出克隆(仅元数据)
git clone --no-checkout https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set packages/my-service
git checkout main
冲突解决
理解冲突标记
<<<<<<< HEAD(或 "ours")
当前分支的更改
=======
来自入站分支的更改
>>>>>>> feature-branch(或 "theirs")
解决策略
# 全部接受我们的(当前分支胜出)
git checkout --ours path/to/file.ts
git add path/to/file.ts# 全部接受他们的(入站分支胜出)
git checkout --theirs path/to/file.ts
git add path/to/file.ts
# 对所有文件接受我们的
git checkout --ours .
git add .
# 使用合并工具
git mergetool
# 查看三方差异(基础、我们的、他们的)
git diff --cc path/to/file.ts
# 显示共同祖先版本
git show :1:path/to/file.ts # 基础(共同祖先)
git show :2:path/to/file.ts # 我们的
git show :3:path/to/file.ts # 他们的
变基冲突工作流
# 变基期间,冲突逐个提交出现
# 1. 在文件中修复冲突
# 2. 暂存修复
git add fixed-file.ts
# 3. 继续到下一个提交
git rebase --continue
# 4. 重复直到完成# 如果解决后提交变为空
git rebase --skip
Rerere(重用已记录的解决方案)
# 全局启用 rerere
git config --global rerere.enabled true# Git 会记住你如何解决冲突
# 下次出现相同冲突时,自动解决
# 查看已记录的解决方案
ls .git/rr-cache/
# 忘记一个错误的解决方案
git rerere forget path/to/file.ts
Stash 模式
# 带消息暂存
git stash push -m "WIP: refactoring auth flow"# 暂存特定文件
git stash push -m "partial stash" -- src/auth.ts src/login.ts
# 暂存包括未跟踪的文件
git stash push -u -m "with untracked"
# 列出暂存
git stash list
# 应用最近的暂存(保留在暂存列表中)
git stash apply
# 应用并从暂存列表中移除
git stash pop
# 应用特定的暂存
git stash apply stash@{2}
# 查看暂存内容
git stash show -p stash@{0}
# 从暂存创建分支
git stash branch new-feature stash@{0}
# 丢弃特定暂存
git stash drop stash@{1}
# 清除所有暂存
git stash clear
Blame 和日志考古
# 查看每行的修改者(带日期)
git blame src/auth.ts# Blame 特定行范围
git blame -L 50,70 src/auth.ts
# 在 blame 中忽略空白更改
git blame -w src/auth.ts
# 查找某行何时被删除(搜索所有历史)
git log -S "function oldName" --oneline
# 查找正则模式何时被添加/删除
git log -G "TODO.hack" --oneline
# 跟踪文件的重命名历史
git log --follow --oneline -- src/new-name.ts
# 显示每行的最后修改提交,忽略移动
git blame -M src/auth.ts
# 显示带文件更改的日志
git log --stat --oneline -20
# 显示影响特定文件的所有提交
git log --oneline -- src/auth.ts
# 显示特定提交的差异
git show abc123
标签和发布
# 创建带注释的标签(发布推荐)
git tag -a v1.2.0 -m "Release 1.2.0: Added auth module"# 创建轻量标签
git tag v1.2.0
# 为过去的提交打标签
git tag -a v1.1.0 abc123 -m "Retroactive tag for release 1.1.0"
# 列出标签
git tag -l
git tag -l "v1."
# 推送标签
git push origin v1.2.0 # 单个标签
git push origin --tags # 所有标签
# 删除标签
git tag -d v1.2.0 # 本地
git push origin --delete v1.2.0 # 远程
技巧
git rebase -i 是最有用的高级 Git 命令。优先学习它。
- 永远不要对已推送到共享分支的提交进行变基。只变基你的本地/功能分支。
git reflog 是你的安全网。如果丢失提交,几乎总能在 90 天内恢复。
git bisect run 配合自动化测试比手动二分搜索更快,且消除人为错误。
- Worktree 比多次克隆更轻量——它们共享
.git 存储。
- 除非有特定原因,优先使用
git subtree 而非 git submodule。Subtree 对协作者更简单。
- 全局启用
rerere。它会记住冲突解决方案,让你永远不用解决相同的冲突两次。
git stash push -m "描述" 比裸 git stash 好得多。当你有 5 个暂存时会感谢自己。
git log -S "字符串"(镐头)是查找函数或变量何时被添加或删除的最快方法。
Advanced git operations for real-world development. Covers interactive rebase, bisect, worktree, reflog recovery, subtrees, submodules, sparse checkout, conflict resolution, and monorepo patterns.
When to Use
- Cleaning up commit history before merging (interactive rebase)
- Finding which commit introduced a bug (bisect)
- Working on multiple branches simultaneously (worktree)
- Recovering lost commits or undoing mistakes (reflog)
- Managing shared code across repos (subtree/submodule)
- Resolving complex merge conflicts
- Cherry-picking commits across branches or forks
- Working with large monorepos (sparse checkout)
Interactive Rebase
Squash, reorder, edit commits
# Rebase last 5 commits interactively
git rebase -i HEAD~5# Rebase onto main (all commits since diverging)
git rebase -i main
The editor opens with a pick list:
pick a1b2c3d Add user model
pick e4f5g6h Fix typo in user model
pick i7j8k9l Add user controller
pick m0n1o2p Add user routes
pick q3r4s5t Fix import in controller
Commands available:
pick = use commit as-is
reword = use commit but edit the message
edit = stop after this commit to amend it
squash = merge into previous commit (keep both messages)
fixup = merge into previous commit (discard this message)
drop = remove the commit entirely
Common patterns
# Squash fix commits into their parent
# Change "pick" to "fixup" for the fix commits:
pick a1b2c3d Add user model
fixup e4f5g6h Fix typo in user model
pick i7j8k9l Add user controller
fixup q3r4s5t Fix import in controller
pick m0n1o2p Add user routes# Reorder commits (just move lines)
pick i7j8k9l Add user controller
pick m0n1o2p Add user routes
pick a1b2c3d Add user model
# Split a commit into two
# Mark as "edit", then when it stops:
git reset HEAD~
git add src/model.ts
git commit -m "Add user model"
git add src/controller.ts
git commit -m "Add user controller"
git rebase --continue
Autosquash (commit messages that auto-arrange)
# When committing a fix, reference the commit to squash into
git commit --fixup=a1b2c3d -m "Fix typo"
# or
git commit --squash=a1b2c3d -m "Additional changes"# Later, rebase with autosquash
git rebase -i --autosquash main
# fixup/squash commits are automatically placed after their targets
Abort or continue
git rebase --abort # Cancel and restore original state
git rebase --continue # Continue after resolving conflicts or editing
git rebase --skip # Skip the current commit and continue
Bisect (Find the Bug)
Binary search through commits
# Start bisect
git bisect start# Mark current commit as bad (has the bug)
git bisect bad
# Mark a known-good commit (before the bug existed)
git bisect good v1.2.0
# or: git bisect good abc123
# Git checks out a middle commit. Test it, then:
git bisect good # if this commit doesn't have the bug
git bisect bad # if this commit has the bug
# Repeat until git identifies the exact commit
# "abc123 is the first bad commit"
# Done — return to original branch
git bisect reset
Automated bisect (with a test script)
# Fully automatic: git runs the script on each commit
# Script must exit 0 for good, 1 for bad
git bisect start HEAD v1.2.0
git bisect run ./test-for-bug.sh# Example test script
cat > /tmp/test-for-bug.sh << 'EOF'
#!/bin/bash
# Return 0 if bug is NOT present, 1 if it IS
npm test -- --grep "login should redirect" 2>/dev/null
EOF
chmod +x /tmp/test-for-bug.sh
git bisect run /tmp/test-for-bug.sh
Bisect with build failures
# If a commit doesn't compile, skip it
git bisect skip# Skip a range of known-broken commits
git bisect skip v1.3.0..v1.3.5
Worktree (Parallel Branches)
Work on multiple branches simultaneously
# Add a worktree for a different branch
git worktree add ../myproject-hotfix hotfix/urgent-fix
# Creates a new directory with that branch checked out# Add a worktree with a new branch
git worktree add ../myproject-feature -b feature/new-thing
# List worktrees
git worktree list
# Remove a worktree when done
git worktree remove ../myproject-hotfix
# Prune stale worktree references
git worktree prune
Use cases
# Review a PR while keeping your current work untouched
git worktree add ../review-pr-123 origin/pr-123# Run tests on main while developing on feature branch
git worktree add ../main-tests main
cd ../main-tests && npm test
# Compare behavior between branches side by side
git worktree add ../compare-old release/v1.0
git worktree add ../compare-new release/v2.0
Reflog (Recovery)
See everything git remembers
# Show reflog (all HEAD movements)
git reflog
# Output:
# abc123 HEAD@{0}: commit: Add feature
# def456 HEAD@{1}: rebase: moving to main
# ghi789 HEAD@{2}: checkout: moving from feature to main# Show reflog for a specific branch
git reflog show feature/my-branch
# Show with timestamps
git reflog --date=relative
Recover from mistakes
# Undo a bad rebase (find the commit before rebase in reflog)
git reflog
# Find: "ghi789 HEAD@{5}: checkout: moving from feature to main" (pre-rebase)
git reset --hard ghi789# Recover a deleted branch
git reflog
# Find the last commit on that branch
git branch recovered-branch abc123
# Recover after reset --hard
git reflog
git reset --hard HEAD@{2} # Go back 2 reflog entries
# Recover a dropped stash
git fsck --unreachable | grep commit
# or
git stash list # if it's still there
git log --walk-reflogs --all -- stash # find dropped stash commits
Cherry-Pick
Copy specific commits to another branch
# Pick a single commit
git cherry-pick abc123# Pick multiple commits
git cherry-pick abc123 def456 ghi789
# Pick a range (exclusive start, inclusive end)
git cherry-pick abc123..ghi789
# Pick without committing (stage changes only)
git cherry-pick --no-commit abc123
# Cherry-pick from another remote/fork
git remote add upstream https://github.com/other/repo.git
git fetch upstream
git cherry-pick upstream/main~3 # 3rd commit from upstream's main
Handle conflicts during cherry-pick
# If conflicts arise:
# 1. Resolve conflicts in the files
# 2. Stage resolved files
git add resolved-file.ts
# 3. Continue
git cherry-pick --continue# Or abort
git cherry-pick --abort
Subtree and Submodule
Subtree (simpler — copies code into your repo)
# Add a subtree
git subtree add --prefix=lib/shared https://github.com/org/shared-lib.git main --squash# Pull updates from upstream
git subtree pull --prefix=lib/shared https://github.com/org/shared-lib.git main --squash
# Push local changes back to upstream
git subtree push --prefix=lib/shared https://github.com/org/shared-lib.git main
# Split subtree into its own branch (for extraction)
git subtree split --prefix=lib/shared -b shared-lib-standalone
Submodule (pointer to another repo at a specific commit)
# Add a submodule
git submodule add https://github.com/org/shared-lib.git lib/shared# Clone a repo with submodules
git clone --recurse-submodules https://github.com/org/main-repo.git
# Initialize submodules after clone (if forgot --recurse)
git submodule update --init --recursive
# Update submodules to latest
git submodule update --remote
# Remove a submodule
git rm lib/shared
rm -rf .git/modules/lib/shared
# Remove entry from .gitmodules if it persists
When to use which
Subtree: Simpler, no special commands for cloners, code lives in your repo.
Use when: shared library, vendor code, infrequent upstream changes.Submodule: Pointer to exact commit, smaller repo, clear separation.
Use when: large dependency, independent release cycle, many contributors.
Sparse Checkout (Monorepo)
Check out only the directories you need
# Enable sparse checkout
git sparse-checkout init --cone# Select directories
git sparse-checkout set packages/my-app packages/shared-lib
# Add another directory
git sparse-checkout add packages/another-lib
# List what's checked out
git sparse-checkout list
# Disable (check out everything again)
git sparse-checkout disable
Clone with sparse checkout (large monorepos)
# Partial clone + sparse checkout (fastest for huge repos)
git clone --filter=blob:none --sparse https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set packages/my-service# No-checkout clone (just metadata)
git clone --no-checkout https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set packages/my-service
git checkout main
Conflict Resolution
Understand the conflict markers
<<<<<<< HEAD (or "ours")
Your changes on the current branch
=======
Their changes from the incoming branch
>>>>>>> feature-branch (or "theirs")
Resolution strategies
# Accept all of ours (current branch wins)
git checkout --ours path/to/file.ts
git add path/to/file.ts# Accept all of theirs (incoming branch wins)
git checkout --theirs path/to/file.ts
git add path/to/file.ts
# Accept ours for ALL files
git checkout --ours .
git add .
# Use a merge tool
git mergetool
# See the three-way diff (base, ours, theirs)
git diff --cc path/to/file.ts
# Show common ancestor version
git show :1:path/to/file.ts # base (common ancestor)
git show :2:path/to/file.ts # ours
git show :3:path/to/file.ts # theirs
Rebase conflict workflow
# During rebase, conflicts appear one commit at a time
# 1. Fix the conflict in the file
# 2. Stage the fix
git add fixed-file.ts
# 3. Continue to next commit
git rebase --continue
# 4. Repeat until done# If a commit is now empty after resolution
git rebase --skip
Rerere (reuse recorded resolutions)
# Enable rerere globally
git config --global rerere.enabled true# Git remembers how you resolved conflicts
# Next time the same conflict appears, it auto-resolves
# See recorded resolutions
ls .git/rr-cache/
# Forget a bad resolution
git rerere forget path/to/file.ts
Stash Patterns
# Stash with a message
git stash push -m "WIP: refactoring auth flow"# Stash specific files
git stash push -m "partial stash" -- src/auth.ts src/login.ts
# Stash including untracked files
git stash push -u -m "with untracked"
# List stashes
git stash list
# Apply most recent stash (keep in stash list)
git stash apply
# Apply and remove from stash list
git stash pop
# Apply a specific stash
git stash apply stash@{2}
# Show what's in a stash
git stash show -p stash@{0}
# Create a branch from a stash
git stash branch new-feature stash@{0}
# Drop a specific stash
git stash drop stash@{1}
# Clear all stashes
git stash clear
Blame and Log Archaeology
# Who changed each line (with date)
git blame src/auth.ts# Blame a specific line range
git blame -L 50,70 src/auth.ts
# Ignore whitespace changes in blame
git blame -w src/auth.ts
# Find when a line was deleted (search all history)
git log -S "function oldName" --oneline
# Find when a regex pattern was added/removed
git log -G "TODO.hack" --oneline
# Follow a file through renames
git log --follow --oneline -- src/new-name.ts
# Show the commit that last touched each line, ignoring moves
git blame -M src/auth.ts
# Show log with file changes
git log --stat --oneline -20
# Show all commits affecting a specific file
git log --oneline -- src/auth.ts
# Show diff of a specific commit
git show abc123
Tags and Releases
# Create annotated tag (preferred for releases)
git tag -a v1.2.0 -m "Release 1.2.0: Added auth module"# Create lightweight tag
git tag v1.2.0
# Tag a past commit
git tag -a v1.1.0 abc123 -m "Retroactive tag for release 1.1.0"
# List tags
git tag -l
git tag -l "v1."
# Push tags
git push origin v1.2.0 # Single tag
git push origin --tags # All tags
# Delete a tag
git tag -d v1.2.0 # Local
git push origin --delete v1.2.0 # Remote
Tips
git rebase -i is the single most useful advanced git command. Learn it first.
- Never rebase commits that have been pushed to a shared branch. Rebase your local/feature work only.
git reflog is your safety net. If you lose commits, they're almost always recoverable within 90 days.
git bisect run with an automated test is faster than manual binary search and eliminates human error.
- Worktrees are cheaper than multiple clones — they share
.git storage.
- Prefer
git subtree over git submodule unless you have a specific reason. Subtrees are simpler for collaborators.
- Enable
rerere globally. It remembers conflict resolutions so you never solve the same conflict twice.
git stash push -m "description" is much better than bare git stash. You'll thank yourself when you have 5 stashes.
git log -S "string" (pickaxe) is the fastest way to find when a function or variable was added or removed.