Git:rebase vs merge / stash / cherry-pick(零基础全攻略)

一句话先记

Git = 给代码拍快照 + 在快照之间来回穿梭。核心三板斧:add(拍照前准备)→ commit(拍一张照)→ push(把照片传到网上)。merge = 两条路合并,rebase = 把一条路的起点挪到另一条路的终点,stash = 暂时把半成品藏起来,cherry-pick = 从别人的分支里挑一个提交拿过来。


先搞懂——Git 到底是什么?

没有 Git 的时候

 
你写了一个文件 index.html
 
第一天:改了 10 行 → 另存为 index_v1.html
 
第二天:又改了 5 行 → 另存为 index_v2.html
 
第三天:又改了一堆 → 另存为 index_最终版.html
 
第四天:又改了一堆 → 另存为 index_最终版2.html
 
第五天:你想找回第二天的版本 → 你翻遍了文件夹,根本分不清哪个是哪个
 
这就是没有版本控制的痛苦。
 

Git 怎么解决的

 
Git = 一个"后悔药"制造机。
 
每当你觉得"这一步代码可以了",你就拍一张快照(commit)。
 
Git 帮你存好这张快照。
 
之后你可以:
 
- 随时回退到任何一张快照
 
- 看看两张快照之间改了啥
 
- 同时开好几条不同的开发线
 

Git 的三个区域


graph LR

    A["工作区<br/>Working Directory<br/>你正在改代码的地方"] -->|"git add"| B["暂存区<br/>Staging Area<br/>准备好要拍照的文件"]

    B -->|"git commit"| C["本地仓库<br/>Repository<br/>拍好的照片存在这"]

    C -->|"git push"| D["远程仓库<br/>Remote<br/>GitHub/GitLab 上的备份"]

    D -->|"git pull"| A

说人话:

 
工作区 = 你的书桌,你正在写作业
 
暂存区 = 你挑了几张写好的作业放桌上,准备交给老师检查
 
本地仓库 = 老师批改完存档在柜子里
 
远程仓库 = 柜子的复印版放在家里保险柜(防止柜子被烧了)
 
git add → 把作业从桌面放到"待批改"的盒子里
 
git commit → 老师批改完存进柜子(生成一个版本号)
 
git push → 复印一份放家里保险柜
 
git pull → 从保险柜把最新版本拿回来
 

最基本操作


sequenceDiagram

    participant W as 工作区

    participant S as 暂存区

    participant R as 本地仓库

    participant P as 远程仓库

    Note over W: 你在 index.html 里改了一行

    W->>S: git add index.html

    Note over S: index.html 进入待提交状态

    S->>R: git commit -m "修复了一个 bug"

    Note over R: 拍了一张快照 版本号 abc123

    R->>P: git push

    Note over P: GitHub 上也存了一份

    P-->>W: git pull

    Note over W: 从 GitHub 拉取最新代码

常用命令

 
git init                → 把当前文件夹变成 Git 仓库(初始化)
 
git status              → 看看现在什么文件被改了
 
git log                 → 看所有历史提交
 
git log --oneline       → 精简版历史
 
git add 文件名           → 把某个文件放到暂存区
 
git add .               → 把所有改了文件放到暂存区
 
git commit -m "说明"     → 拍一张快照
 
git push                → 把本地照片传到 GitHub
 
git pull                → 从 GitHub 拉最新代码
 

分支——Git 最强的能力

为什么需要分支

 
你在写一个网页。
 
突然老板说:"加个支付功能!"(需要两天)
 
同时:"立刻把首页的错别字改掉!"(需要 30 秒)
 
没有分支:
 
你改了首页的错别字,想上线——但支付功能写了一半,不能上线。
 
你卡住了。要么等支付写完,要么把支付代码删了。
 
有分支:
 
- 建一个分支叫 feature/payment,专门写支付
 
- 切回主分支,改错别字,上线
 
- 切回 feature/payment,继续写支付
 
两个开发线互不干扰。
 

graph TD

    A["main 分支<br/>稳定版本"] -->|"切出 feature/payment"| B["feature/payment<br/>写支付功能"]

    A -->|"切出 hotfix/typo"| C["hotfix/typo<br/>改错别字"]

    B -->|"写了两天"| D["支付写完了"]

    D -->|"合并回 main"| E["main 有了支付功能"]

    C -->|"30 秒改完"| F["错别字修了"]

    F -->|"合并回 main"| E

分支的本质

 
Git 的分支 = 一个指针,指着某次提交。
 
main 分支 → 指向提交 #5
 
feature 分支 → 指向提交 #3(从 #3 那里分出来的)
 
你每提交一次,分支指针就往前移一格。
 

分支命令

 
git branch               → 查看所有分支(* 表示当前所在分支)
 
git branch 分支名         → 创建一个新分支
 
git checkout 分支名       → 切换到某个分支
 
git switch 分支名         → 切换到某个分支(新版 Git 推荐)
 
git checkout -b 分支名    → 创建并切换到新分支
 
git branch -d 分支名      → 删除分支
 

核心操作一:merge——合并

场景

 
你在 main 分支上,建了一个 feature/login 分支写登录功能。
 
写完了,想把它合并回 main。
 

两种 merge 方式


graph TD

    A["master: 提交 #1"] --> B["master: 提交 #2"]

    B --> C["master: 提交 #3"]

    B --> D["feature: 提交 #4"]

    D --> E["feature: 提交 #5"]

    E --> F["在 master 上 git merge feature"]

    C --> F

    E --> F

    F --> G["合并产生了新的提交 #6<br/>包含 master 和 feature 的所有代码"]

实际操作

 
# 确保你在 main 分支上
 
git checkout main
 
# 把 feature/login 合并进来
 
git merge feature/login
 
# 如果有冲突 → Git 说"有冲突,手动解决" → 修完文件 → git add → git commit
 

冲突是什么

 
你和同事同时改了同一个文件的同一行。
 
你改的:
 
用户名称:显示中文
 
同事改的:
 
用户名称:显示英文
 
Git 不知道听谁的 → 停下来让你自己选。
 
冲突文件里会显示:
 
<<<<<<< HEAD
 
用户名称:显示中文
 
=======
 
用户名称:显示英文
 
>>>>>>> feature/login
 
你需要手动改成你要的版本,然后:
 
git add 文件
 
git commit -m "解决了冲突"
 

核心操作二:rebase——变基

rebase 是什么

 
merge = "把两条路汇合到一起,产生一个合并点"
 
rebase = "把你这条路的起点,移到另一条路的终点"
 
结果一样(代码合并了),但历史更干净。
 

graph TD

    subgraph "rebase 前 和 merge 一样"

        A["#1"] --> B["#2"]

        B --> C["#3 main"]

        B --> D["#4 feature"]

        D --> E["#5 feature"]

    end

    subgraph "git rebase main 之后"

        A -...-> B -...-> C1["#3 main"]

        C1 --> D1["#4' feature"]

        D1 --> E1["#5' feature"]

    end

    subgraph "merge 的结果"

        C --> F["merge commit #6"]

        E --> F

    end

    subgraph "rebase + fast-forward merge 的结果"

        C1 --> D1 --> E1

    end

merge 和 rebase 的区别


graph LR

    A["git merge feature"] --> B["产生一个新的合并提交<br/>有分支痕迹<br/>历史能看到'这里合并过一次'"]

    C["git rebase main"] --> D["没有合并提交<br/>历史是一条直线<br/>看起来像顺序开发的"]

    B --> E["优点:保留真实历史<br/>缺点:提交图复杂"]

    D --> F["优点:历史干净线性<br/>缺点:改写了历史<br/>(不要对公共分支用)"]

rebase 怎么操作

 
你当前在 feature 分支上
 
git checkout feature
 
git rebase main
 
如果有冲突:
 
# 想放弃 → git rebase --abort
 
rebase 完成后,切回 main 合并
 
git checkout main
 
git merge feature
 
# 此时 main 会直接快进(fast-forward),没有额外的合并提交
 

rebase 黄金法则

 
永远不要对公共分支(main / master / develop)执行 rebase!
 
为什么?
 
rebase 改写了提交历史。
 
如果你 rebase 了一个别人也在用的分支,
 
别人下一次 pull 会看到完全不同的历史,直接炸裂。
 
简而言之:
 
- 你自己的分支(没人用的)→ 随便 rebase
 
- 公共分支(多人协作用)→ 只能用 merge
 

merge vs rebase 怎么选

mergerebase
历史记录保留真实分支结构,看起来像”地铁线路图”线性历史,看起来像”单轨道”
冲突处理一次解决所有冲突每个提交可能都要解决一次冲突
安全性✅ 安全,不改历史❌ 改写了历史,不能用于公共分支
场景合并公共分支(main)整理自己的分支历史
说人话”我们在这里合并了分支""假装这些提交是顺序发生的”

公司实际用法:

 
1. 在自己的 feature 分支上开发
 
2. 用 rebase 把自己的提交整理干净(把多个小提交合成一个)
 
3. git rebase main(把 main 的最新代码同步过来)
 
4. git checkout main && git merge feature(合并回主分支)
 
5. 你的 feature 分支删掉
 
主分支上看到的结果:线性历史,没有乱七八糟的分支痕迹。
 

核心操作三:stash——藏起来

什么时候用 stash

 
你正在 feature 分支上改代码,改到一半。
 
突然:线上有个紧急 bug,你得切回 main 分支去修。
 
问题:你代码改了一半,没写完不能 commit,但切换分支必须工作区干净。
 

stash 解决

 
git stash       → 把当前所有未提交的改动"藏起来"
 
                 工作区变干净了,回到修改前的状态
 
git checkout main    → 切回 main,修 bug
 
git commit -m "fix"
 
git push
 
git checkout feature → 切回 feature
 
git stash pop        → 把藏起来的改动拿回来,继续写
 

stash 完整用法

 
git stash                    → 藏起来(默认只藏 tracked 文件)
 
git stash -u                 → 藏起来(包括新文件)
 
git stash list               → 看看藏了多少次
 
git stash pop                → 拿回最后一次藏的
 
git stash apply              → 拿回但不从列表里删除
 
git stash drop               → 丢掉某次 stash
 
git stash pop stash@{2}      → 拿回指定的 stash
 

说人话:

 
stash = 你写作业写一半,老师突然叫你去办公室。
 
        你把写到一半的作业塞进抽屉里(stash)。
 
        从办公室回来后,拉开抽屉继续写(stash pop)。
 

stash 和 branch 的区别

 
stash = 临时保存,不产生提交(半成品)
 
branch = 长期开发线,产生提交
 
需要长期开发 → 建分支
 
临时改个紧急 bug → stash + 切分支 + 改完切回来 + stash pop
 

核心操作四:cherry-pick——挑一个

什么时候用

 
你在 main 分支上。
 
同事在 feature/payment 分支上修了一个 bug,提交是 abc123。
 
你想把这个 bug 修复拿过来,但不想把整个 feature/payment 合并过来。
 
cherry-pick = 只挑那一个提交,别的不要。
 

graph TD

    A["main 分支"] --> B["提交 #1"]

    B --> C["提交 #2"]

    C --> D["提交 #3"]

    E["feature 分支"] --> F["提交 #4"]

    F --> G["提交 #5 ← 修复了一个 bug"]

    G --> H["提交 #6"]

    G -.->|"git cherry-pick #5"| I["main 上也有了这个修复"]

    I --> D

操作

 
# 先看看 feature 分支的提交历史
 
git log feature --oneline
 
# 切换到 main 分支
 
git checkout main
 
# 只挑这一个提交过来
 
git cherry-pick abc1234
 
# 想放弃 → git cherry-pick --abort
 

cherry-pick 使用场景

 
1. 紧急修复上线后,忘了合并到其他分支
 
   → cherry-pick 到其他分支
 
2. 测试环境验证了一个功能,只想把这个功能上线
 
   → cherry-pick 这个功能的提交到 main
 
3. 从别人的分支里"偷"一个提交
 
   → cherry-pick
 

其他常用 Git 操作

push vs pull vs fetch


graph LR

    A["本地仓库"] -->|"git push"| B["远程仓库"]

    B -->|"git fetch"| C["远程跟踪分支<br/>origin/main<br/>(只下载 不合并)"]

    B -->|"git pull = fetch + merge"| A["本地仓库<br/>(下载 + 自动合并)"]

 
git push            → 把本地提交传到远程
 
git fetch           → 把远程最新提交下载到本地,但不合并
 
git pull            → git fetch + git merge(下载并合并)
 

fetch 和 pull 的区别:

 
git fetch:先看看别人改了啥,但不动你的代码。
 
git pull:直接把你拉进去,代码自动合并。
 
建议:先用 git fetch 看看情况,再决定要不要 merge。
 

reset vs revert——回退

 
两个都是"回到过去",但方式不一样。
 
revertreset
做了什么创建一个”反向提交”来抵消之前的提交直接删掉之前的提交
改历史吗❌ 不改,新增一个提交✅ 改了,删除了提交
安全吗✅ 安全,可以 push❌ 危险,不能用于公共分支
说人话”我撤销了之前的修改”(历史里能看到撤销记录)“假装之前的修改没发生过”(历史里直接消失了)

graph TD

    subgraph "revert"

        A1["#1"] --> A2["#2"] --> A3["#3 想撤销这个"] --> A4["#4 git revert #3<br/>产生一个新的提交<br/>内容是 #3 的反向操作"]

    end

    subgraph "reset"

        B1["#1"] --> B2["#2"] --> B3["#3 想删掉这个"]

        B3 -.->|"git reset --hard HEAD~1"| B4["直接回到 #2<br/>#3 不存在了"]

    end

 
# revert(安全)
 
git revert abc1234          → 产生一个新的提交,抵消 abc1234 的改动
 
git push                    → 可以正常 push
 
# reset(危险,只能用于本地)
 
git reset --soft HEAD~1     → 撤销最后一次提交,但保留改动在暂存区
 
git reset --mixed HEAD~1    → 撤销最后一次提交,保留改动在工作区(默认)
 
git reset --hard HEAD~1     → 撤销最后一次提交,改动全丢掉
 

HEAD 是什么

 
HEAD = 你当前在哪里的指针。
 
每次你 git checkout main → HEAD 指向 main 分支
 
每次你 git checkout feature → HEAD 指向 feature 分支
 
HEAD 永远指向你"当前所在的位置"。
 

graph LR

    A["#1"] --> B["#2"]

    B --> C["#3 ← main, HEAD"]

    B --> D["#4 ← feature"]

    E["HEAD 指向 main 时 → 你在 main 分支上"] -.-> C

    F["git checkout feature 之后<br/>HEAD 指向 feature → 你在 feature 上"] -.-> D

detached HEAD(分离头指针)——你”悬空”了

正常情况:HEAD 指向一个分支名。

 
HEAD → main → #3
 
HEAD → feature → #4
 
你每次 git commit,分支往前移一格,HEAD 跟着分支走。
 
就像你坐在一辆公交车(分支)上,车往前开,你跟着往前。
 

detached HEAD:HEAD 直接指向一个提交,不在任何分支上。

 
HEAD → abc1234(某个历史提交,不在任何分支上)
 
你下车了,不在任何公交车上,自己站在路边(某个历史提交上)。
 

怎么进入 detached HEAD:

 
git checkout abc1234    ← 直接跳到某个历史提交的 hash
 
或者更常见的误操作:
 
git checkout HEAD~3    ← 回到 3 次提交之前
 
git checkout v1.0      ← 切到某个标签(tag)
 

为什么危险:

 
你在 detached HEAD 状态下做了提交:
 
HEAD → #5(你新提交的)
 
        你不在任何分支上!
 
然后你切走了:
 
git checkout main
 
此时 #5 没有任何分支指向它。
 
没有任何分支包含 #5 这个提交。
 
你再也找不回来了(除非你用 git reflog)。
 

怎么解决:

 
# 方案 1:你在 detached HEAD 下已经写了代码
 
git checkout -b new-branch
 
# 方案 2:还没写代码,只是看看历史
 
git checkout main  # 直接切走就行,没损失
 

类比:

 
正常(在分支上):
 
你坐在公交车上,车停你就停,车走你就走。
 
你下车了再上车,车还是在原来的位置。
 
detached HEAD:
 
你从公交车上下车,站在路边(某个历史站点)。
 
你在路边搭了个帐篷(提交了代码)。
 
然后你走开了(切到别的分支)。
 
帐篷就留在路边了——没人管,下次清洁工来就扔了。
 
解决方案:
 
在搭帐篷的地方插个牌子(git checkout -b 新分支名),
 
变成一辆新的公交车。
 

实际最常遇到的 detached HEAD:

 
你 git checkout 了一个历史提交的 hash 来"看看以前长啥样"。
 
Git 提示你:
 
You are in 'detached HEAD' state.
 
(你处于分离头指针状态)
 
这时候你只要不做提交,切回 main 就完事了。
 
如果做了提交,记得建分支保住它。
 

综合工作流示例

 
一个完整的需求开发流程:
 

sequenceDiagram

    participant D as 开发者

    participant G as Git(本地仓库)

    participant R as GitHub(远程仓库)

    Note over D: 开始开发新功能

    D->>G: git checkout main

    G-->>D: 已切换到 main

    D->>G: git pull

    G->>R: fetch 最新代码

    R-->>G: 下载完成

    G-->>D: main 已是最新

    D->>G: git checkout -b feature/login

    G-->>D: 已创建并切换到 feature

    Note over D: 写了一上午 改到一半

    D->>G: git stash

    G-->>D: 改动已藏起来

    D->>G: git checkout main

    D->>G: 改紧急 bug

    D->>G: git add . && git commit -m "fix"

    G-->>D: 创建了提交

    D->>G: git push

    G->>R: 上传提交

    R-->>G: 推送成功

    D->>G: git checkout feature/login

    D->>G: git stash pop

    G-->>D: 改动已恢复

    Note over D: 写完了

    D->>G: git add . && git commit -m "完成登录功能"

    G-->>D: 创建了提交

    D->>G: git checkout main

    D->>G: git pull

    G->>R: fetch

    R-->>G: 下载完成

    G-->>D: main 已更新

    D->>G: git checkout feature/login

    D->>G: git rebase main

    Note over G: 把 main 最新代码同步过来<br/>有冲突在这解决

    D->>G: git push origin feature/login

    G->>R: 上传 feature 分支

    R-->>G: 推送成功

    D->>R: 创建 Pull Request<br/>请求把 feature/login 合并到 main

    Note over R: 同事审查代码

    R-->>D: 审查通过

    D->>R: 点击 Merge(或同事点)

    Note over R: GitHub 自动合并到 main

    D->>G: git checkout main

    D->>G: git pull

    G->>R: fetch 最新的 main

    R-->>G: 已更新

    G-->>D: main 已包含你的新功能


常见问题

Q1:rebase 和 merge 到底怎么选?

 
merge = 保留真实历史,适合公共分支
 
rebase = 整理历史,适合自己的私有分支
 
公司实践:
 
1. 合并到 main → 用 merge
 
2. 整理自己分支 → 用 rebase
 
3. 永远不要在公共分支上 rebase
 
结论:开发用 merge,整理历史用 rebase,无论如何不 rebase 公共分支。
 

Q2:怎么解决冲突?

 
1. git pull(或者 git merge / rebase)时报冲突
 
2. 打开冲突的文件,看到:
 
   <<<<<<< HEAD
 
   你的代码
 
   =======
 
   别人的代码
 
   >>>>>>> branch-name
 
3. 手动改成你想要的样子
 
4. git add 文件
 
5. git commit(merge 时)/ git rebase --continue(rebase 时)
 

Q3:git pull 和 git fetch 有什么区别?

 
git fetch = 去看看远程有啥新东西,但不拉到你代码里
 
git pull = fetch + 自动合并
 
建议:
 
先 git fetch,看看别人改了啥,
 
确认没问题再 git pull(或者手动 merge)。
 

Q4:cherry-pick 什么时候用?

 
你同事修了一个 bug,提交 hash 是 abc123。
 
你不想合并他整个分支(可能他还有没写完的代码),
 
只想拿这个 bug 修复。
 
git cherry-pick abc123
 

Q5:git reset 和 git revert 什么时候用?

 
还没 push(本地 commit 错了):git reset --soft HEAD~1
 
已经 push(远程也有这个提交):git revert abc123
 
原则:没 push 的可以 reset,已 push 的只能 revert。
 

一句话总结

Git = 拍快照 + 在快照间穿梭。merge = 合并分支保留历史痕迹,rebase = 重放提交保持线性历史(别用在公共分支)。stash = 半成品临时藏起来,cherry-pick = 只挑一个提交过来。学习重点:merge vs rebase 选择、冲突解决、reset vs revert 区别。

速记卡(面试闪卡)

Q1:一句话讲清「Git:rebase vs merge / stash / cherry-pick(零基础全攻略)」到底是什么?

A:Git 四大操作:merge 合并留痕、rebase 重放保线性、stash 暂藏半成品、cherry-pick 单挑提交。

Q2:merge vs rebase 怎么选 —— 怎么理解?

A:merge 产生一个合并提交,保留真实分支历史(像地铁线路图),安全不改历史;rebase 把你的提交”挪”到目标分支顶端重放,历史变直线(像单轨),但改写了历史。黄金法则:公共分支只用 merge,自己的分支随便 rebase。这叫 rebase(变基)vs merge(合并)。

Q3:stash 临时藏改动 —— 怎么理解?

A:改到一半要切分支修紧急 bug,又不能 commit 半成品——git stash 把改动塞进抽屉,工作区干净了,修完回来 stash pop 继续。就像写作业写一半被叫走,把本子塞抽屉,回来接着写。这是 work-in-progress 的临时暂存。

Q4:cherry-pick 挑单个提交 —— 怎么理解?

A:同事修了个 bug 但你不想合并他整个分支,只想要那一个提交——git cherry-pick <hash> 把那个提交”复制”到你当前分支。像从别人盘里只夹一块肉,不端整盘。这叫 cherry-pick(遴选提交)。

Q5:reset vs revert 回退 —— 怎么理解?

A:两者都”回到过去”:revert 新建一个反向提交抵消改动,不改历史、能 push(安全);reset 直接删掉提交,改写历史、不能用于公共分支(危险)。一句话:没 push 用 reset,已 push 用 revert。这叫 revert(安全回退)vs reset(危险回退)。

Q6:核心速记主线有哪些?

  • merge:留合并提交,保留历史,公共分支用

  • rebase:重放提交变线性,改写历史,私有分支用

  • stash:半成品临时藏,pop 取回

  • cherry-pick / reset vs revert:单挑提交 / 安全回退 vs 危险回退

口诀

A:merge 合并留痕迹,

rebase 变基线整齐;

stash 暂存改动避,

revert 安全 reset 弃。

相关链接