Productivity · #git#workflow#rebase

Git高级工作流:rebase/cherry-pick/bisect

2025.10.08 7 min 2.8k
// 目录 · contents

前言

Git的基础命令(add、commit、push、pull)足以应对日常开发。但在复杂的团队协作中,掌握rebase、cherry-pick、bisect等高级操作,能让你更高效地管理代码历史和排查问题。本文将深入这些高级Git技巧和Git的内部机制。

Git内部对象模型

理解Git的内部结构有助于理解高级操作的本质。

graph TB
    subgraph "Git Objects"
        COMMIT[Commit Object<br>tree: abc123<br>parent: def456<br>author: ...<br>message: ...]
        TREE[Tree Object<br>blob a1b2: README.md<br>blob c3d4: main.rs<br>tree e5f6: src/]
        BLOB1[Blob Object<br>文件内容]
        BLOB2[Blob Object<br>文件内容]
    end

    COMMIT --> TREE
    TREE --> BLOB1
    TREE --> BLOB2

    subgraph "References"
        HEAD[HEAD → refs/heads/main]
        MAIN[refs/heads/main → commit hash]
        TAG[refs/tags/v1.0 → commit hash]
    end

    HEAD --> MAIN
    MAIN --> COMMIT
1
2
3
4
5
6
7
8
# 查看Git对象
git cat-file -t HEAD # 类型(commit)
git cat-file -p HEAD # 内容
git cat-file -p HEAD^{tree} # 查看commit对应的tree

# 查看引用
git show-ref
cat .git/HEAD # 查看HEAD指向

Git有四种对象类型: - Blob:文件内容 - Tree:目录结构(包含blob和其他tree的引用) - Commit:快照(指向tree,包含父commit、作者、消息) - Tag:带注解的标签

Interactive Rebase

交互式rebase是整理提交历史的最强大工具。

基本用法

1
2
3
4
5
# 对最近N个commit进行交互式rebase
git rebase -i HEAD~5

# 对某个分支基点以后的commit进行rebase
git rebase -i main

编辑器中的操作选项:

1
2
3
4
5
6
7
8
pick   abc1234 feat: add user model         # 保留
reword def5678 fix: typo # 修改commit message
edit ghi9012 feat: add API endpoint # 停下来修改
squash jkl3456 fix: address review comments # 合并到上一个commit
fixup mno7890 fix: another small fix # 合并但丢弃message
drop pqr1234 temp: debug logging # 删除此commit

# 也可以调整顺序,直接移动行

常见场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 场景1: 合并多个小commit为一个
git rebase -i HEAD~4
# 将后三个commit改为squash或fixup

# 场景2: 修改历史commit的message
git rebase -i HEAD~3
# 将目标commit改为reword

# 场景3: 删除错误的commit
git rebase -i HEAD~5
# 删除或drop对应行

# 场景4: 拆分一个大commit
git rebase -i HEAD~3
# 将目标commit改为edit
# rebase停下后:
git reset HEAD~1 # 撤销commit但保留修改
git add file1.rs
git commit -m "part 1"
git add file2.rs
git commit -m "part 2"
git rebase --continue
graph LR
    subgraph "Rebase前"
        A1[A] --> B1[B: feat] --> C1[C: fix typo] --> D1[D: more fix] --> E1[E: refactor]
    end

    subgraph "Rebase后 (squash C,D into B)"
        A2[A] --> B2["B': feat (含修复)"] --> E2["E': refactor"]
    end

Rebase vs Merge

graph TB
    subgraph "Merge: 保留分支历史"
        M_A[A] --> M_B[B]
        M_A --> M_C[C]
        M_B --> M_D[D]
        M_C --> M_E[E]
        M_D --> M_F[Merge Commit]
        M_E --> M_F
    end

    subgraph "Rebase: 线性历史"
        R_A[A] --> R_C[C] --> R_E[E] --> R_B["B'"] --> R_D["D'"]
    end
1
2
3
4
5
6
7
8
9
10
# Merge: 保留完整的分支历史
git checkout main
git merge feature-branch

# Rebase: 将feature分支的commit移到main最新位置
git checkout feature-branch
git rebase main
# 然后fast-forward merge
git checkout main
git merge feature-branch # fast-forward,无merge commit

黄金法则:不要rebase已经推送到远程的公共分支。Rebase会改写提交历史,在共享分支上这样做会导致协作问题。

Cherry-pick

Cherry-pick从另一个分支选取特定commit应用到当前分支。

graph LR
    subgraph "Before cherry-pick"
        A[A] --> B[B] --> C[C]
        A --> D[D] --> E[E] --> F[F]
    end

    subgraph "After cherry-pick E to main"
        A2[A] --> B2[B] --> C2[C] --> E2["E' (cherry-picked)"]
        A2 --> D2[D] --> E3[E] --> F2[F]
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 选取单个commit
git cherry-pick abc1234

# 选取多个commit
git cherry-pick abc1234 def5678

# 选取范围(不包含起始commit)
git cherry-pick abc1234..def5678

# 选取范围(包含起始commit)
git cherry-pick abc1234^..def5678

# 只应用变更,不自动commit
git cherry-pick --no-commit abc1234

# 解决冲突后继续
git cherry-pick --continue

# 放弃cherry-pick
git cherry-pick --abort

Cherry-pick使用场景

1
2
3
4
5
6
7
8
9
10
11
12
# 场景1: 将hotfix从release分支带回main
git checkout main
git cherry-pick release-hotfix-commit-hash

# 场景2: 从废弃的分支中拯救有用的commit
git cherry-pick old-branch~2

# 场景3: 将特定feature commit应用到多个版本分支
git checkout release/1.0
git cherry-pick feature-commit
git checkout release/2.0
git cherry-pick feature-commit

Git Bisect

Bisect使用二分查找定位引入bug的commit。

graph LR
    C1[v1.0<br>Good] --> C2[...] --> C3[...] --> C4[???] --> C5[...] --> C6[...] --> C7[v2.0<br>Bad]

    C4 --> |bisect测试| RESULT{测试结果}
    RESULT --> |good| RIGHT[搜索右半部分]
    RESULT --> |bad| LEFT[搜索左半部分]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 手动bisect
git bisect start
git bisect bad # 标记当前(或指定)commit为bad
git bisect good v1.0 # 标记已知正确的commit

# Git自动checkout中间点,测试后标记
git bisect good # 如果这个commit没有bug
git bisect bad # 如果这个commit有bug

# 重复直到找到第一个引入bug的commit
# Git输出: abc1234 is the first bad commit

# 结束bisect
git bisect reset

# 自动bisect(使用脚本)
git bisect start HEAD v1.0
git bisect run ./test.sh
# test.sh返回0表示good,返回1表示bad
# Git自动二分查找
1
2
3
4
5
6
7
8
9
10
# 实际例子:找到引入测试失败的commit
git bisect start
git bisect bad HEAD # 当前测试失败
git bisect good v1.5 # v1.5时测试通过

# 自动执行测试
git bisect run cargo test -- test_user_login

# 完成后
git bisect reset

Reflog:Git的”撤销”历史

Reflog记录了HEAD和分支引用的所有变动,是恢复误操作的救命工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 查看reflog
git reflog
# 输出:
# abc1234 HEAD@{0}: commit: add feature
# def5678 HEAD@{1}: rebase: squash
# ghi9012 HEAD@{2}: checkout: moving from main to feature
# jkl3456 HEAD@{3}: commit: old commit that was rebased away

# 恢复误删的commit
git checkout -b recovery HEAD@{3}

# 恢复误操作的rebase
git reset --hard HEAD@{1}

# 查看特定分支的reflog
git reflog show feature-branch

# 按时间查找
git reflog --since="2 hours ago"
flowchart TB
    DISASTER[Git灾难] --> Q1{git reflog<br>能找到旧commit?}
    Q1 -->|是| RESET["git reset --hard HEAD@{N}<br>或 git checkout -b rescue HEAD@{N}"]
    Q1 -->|否| Q2{文件在工作区?}
    Q2 -->|是| STASH[git stash / 手动备份]
    Q2 -->|否| FSCK["git fsck --lost-found<br>查找悬垂对象"]
    FSCK --> RECOVER["git show dangling-commit-hash"]

Stash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 基本stash操作
git stash # 暂存工作区修改
git stash pop # 恢复并删除最新stash
git stash apply # 恢复但保留stash

# 带消息的stash
git stash push -m "WIP: user auth feature"

# stash未追踪的文件
git stash push -u -m "including untracked"

# stash特定文件
git stash push -m "partial stash" src/main.rs src/config.rs

# 查看stash列表
git stash list
# stash@{0}: On feature: WIP: user auth
# stash@{1}: On main: quick fix attempt

# 查看stash内容
git stash show stash@{0} # 概览
git stash show -p stash@{0} # 详细diff

# 应用特定stash
git stash apply stash@{1}

# 删除stash
git stash drop stash@{0} # 删除特定
git stash clear # 清空所有

# 从stash创建分支
git stash branch new-branch stash@{0}

Worktree

Worktree允许同时检出多个分支到不同目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 添加工作树
git worktree add ../hotfix-branch hotfix/v1.2
# 在../hotfix-branch目录检出hotfix/v1.2分支

# 创建新分支的工作树
git worktree add -b feature/new ../feature-work main

# 列出所有工作树
git worktree list
# /path/to/repo abc1234 [main]
# /path/to/hotfix-branch def5678 [hotfix/v1.2]

# 删除工作树
git worktree remove ../hotfix-branch

# 使用场景:
# 1. 在修feature时需要紧急修hotfix,不用stash
# 2. 同时对比两个分支的运行结果
# 3. 运行长时间测试时切换到另一个分支工作
graph TB
    REPO[".git (共享)"] --> WT1[Main Worktree<br>/project<br>branch: main]
    REPO --> WT2[Linked Worktree<br>/project-hotfix<br>branch: hotfix/v1.2]
    REPO --> WT3[Linked Worktree<br>/project-feature<br>branch: feature/new]

Merge策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Fast-forward merge(线性历史)
git merge --ff-only feature # 只允许fast-forward

# No-fast-forward merge(总是创建merge commit)
git merge --no-ff feature # 保留分支拓扑信息

# Squash merge(合并为单个commit)
git merge --squash feature # 将所有变更合并到工作区
git commit -m "feat: merged feature branch"

# Ours策略(保留当前分支,忽略对方)
git merge -s ours old-branch # 标记为已合并但不应用变更

# 解决冲突
git merge feature
# 冲突后
git mergetool # 使用合并工具
# 或手动编辑冲突文件
git add resolved-file.rs
git merge --continue

合并策略选择

flowchart TB
    START[合并分支] --> Q1{feature分支?}
    Q1 -->|长期feature| NOFF["--no-ff<br>保留分支历史"]
    Q1 -->|短期修复| Q2{commit数量?}
    Q2 -->|1-2个| FF["--ff-only<br>(先rebase)"]
    Q2 -->|多个| SQUASH["--squash<br>合并为单个commit"]

实用技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 查看谁修改了某一行
git blame src/main.rs
git blame -L 10,20 src/main.rs # 只看10-20行

# 查看文件在特定commit时的内容
git show HEAD~5:src/main.rs

# 搜索commit消息
git log --grep="fix login"

# 搜索代码变更
git log -S "function_name" # 查找添加/删除某字符串的commit
git log -G "regex_pattern" # 正则搜索

# 查看两个分支的差异
git log main..feature # feature有但main没有的commit
git log main...feature # 两者独有的commit
git diff main...feature # feature分支上的改动

# 清理:删除已合并的本地分支
git branch --merged main | grep -v main | xargs git branch -d

# 清理:删除远程已删除的跟踪分支
git fetch --prune

# 查看大文件
git rev-list --objects --all | \
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
sed -n 's/^blob //p' | sort -rnk2 | head -20

推荐的Git配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ~/.gitconfig
[alias]
st = status -sb
lg = log --oneline --graph --decorate --all
co = checkout
br = branch
cp = cherry-pick
unstage = reset HEAD --
last = log -1 HEAD
diff-stat = diff --stat

[pull]
rebase = true # pull时默认rebase

[rebase]
autoSquash = true # 自动识别fixup!/squash! commit
autoStash = true # rebase前自动stash

[merge]
conflictstyle = diff3 # 显示三方diff

[diff]
algorithm = histogram # 更好的diff算法

[rerere]
enabled = true # 记录冲突解决方案,自动应用

总结

Git高级操作的核心要点:

  1. Interactive Rebase:整理提交历史的最强工具,合并、拆分、重排commit
  2. Cherry-pick:从其他分支精确选取commit,适合hotfix回移
  3. Bisect:二分查找定位引入bug的commit,支持自动化脚本
  4. Reflog:Git的”后悔药”,几乎所有误操作都可以通过reflog恢复
  5. Worktree:同时检出多个分支,无需stash切换
  6. 黄金法则:不要rebase公共分支,不要force push到共享分支

掌握这些高级操作,能让你在复杂的团队协作中游刃有余,高效管理代码历史。

作者 · authorzt
发布 · date2025-10-08
篇幅 · length2.8k 字 · 7 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论