84 lines
4.3 KiB
Markdown
84 lines
4.3 KiB
Markdown
---
|
||
id: devops.git.merge-rebase-recovery
|
||
title: 合并、变基和撤销:merge / rebase / cherry-pick、revert / reset、reflog 找回
|
||
type: lesson
|
||
domain: devops
|
||
stack: [git]
|
||
tags: [merge, rebase, cherry-pick, revert, reset, reflog, stash]
|
||
level: intermediate
|
||
prerequisites: [devops.git.branch-commit]
|
||
est_hours: 2
|
||
related: [devops.git.scenario.hotfix-and-leaked-secret, devops.git.interview]
|
||
week: [8, 15]
|
||
order: 2
|
||
---
|
||
|
||
# 合并、变基和撤销:merge / rebase / cherry-pick、revert / reset、reflog 找回
|
||
|
||
## 解释说明
|
||
|
||
**merge vs rebase**
|
||
|
||
- `git merge feat`:把两个分支的历史合在一起,可能产生一个合并提交;历史真实反映“并行开发”,但图形会交错。`--no-ff` 强制生成合并提交,记录“这里合入了一个功能”;`--squash` 把功能分支压成一个提交。
|
||
- `git rebase main`:把当前分支的提交“搬到” main 最新提交之后重新应用,历史是一条直线;但**会生成新的提交(哈希变了)**。
|
||
- 原则:**只 rebase 自己的、还没被别人基于它工作的分支**;公共分支(main、已共享的分支)不要 rebase。
|
||
- `git rebase -i`:交互式整理提交(合并、改信息、删除、调整顺序),合并请求提交前整理用。
|
||
|
||
**cherry-pick**:把某个提交复制到当前分支,常用于把热修复同时带到 release 和 main。
|
||
|
||
**撤销**
|
||
|
||
| 场景 | 命令 | 说明 |
|
||
| --- | --- | --- |
|
||
| 工作区改乱了 | `git restore 文件` | 丢弃未暂存的修改 |
|
||
| 误 `add` 了 | `git restore --staged 文件` | 取消暂存,修改保留 |
|
||
| 最后一次提交信息写错 / 漏文件(未推送) | `git commit --amend` | 改写最后一个提交 |
|
||
| 撤销本地提交(未推送) | `git reset --soft HEAD~1`(保留改动)/ `--hard`(丢弃改动) | 改写历史 |
|
||
| 撤销**已推送**的提交 | `git revert 提交` | 生成一个反向提交,不改写历史,团队协作的安全做法 |
|
||
|
||
**reflog**:记录 HEAD 每次移动的位置(本地,默认保留约 90 天)。`reset --hard`、删分支、错误的 rebase 之后,都可以在 reflog 里找到原来的提交并恢复。**只要提交过,就几乎丢不了。**
|
||
|
||
**stash**:临时保存未提交的改动,`git stash push -m "说明"`、`git stash pop`。
|
||
|
||
## 案例
|
||
|
||
- 合并请求前,用 `rebase -i` 把“WIP”“修正拼写”这类提交合并成两三个有意义的提交,再 `rebase main` 解决冲突,评审者看到的历史清晰。
|
||
- 上线后发现某个提交引入 bug,main 已经被多人拉取:用 `git revert`,不要 reset 后强推。
|
||
- `reset --hard` 后发现删掉了还需要的提交:`git reflog` 找到哈希,`git reset --hard 哈希` 或 `git branch rescue 哈希` 恢复。
|
||
|
||
## 代码示例
|
||
|
||
下面这些操作在 [examples/git-lab/lab.py](examples/git-lab/lab.py) 里会在临时仓库中自动执行并验证:
|
||
|
||
```bash
|
||
# 功能分支同步最新 main(自己的分支,可以 rebase)
|
||
git switch feat/login
|
||
git fetch origin
|
||
git rebase origin/main # 有冲突:解决后 git add 文件 && git rebase --continue;放弃:git rebase --abort
|
||
git push --force-with-lease # 改写过历史的个人分支推送;比 --force 安全:远端有别人的新提交时会拒绝
|
||
|
||
# 撤销已推送的提交
|
||
git revert a1b2c3d
|
||
|
||
# 误 reset 之后找回
|
||
git reflog # 找到 “commit: feat: 重要改动” 那一行的哈希
|
||
git reset --hard 0ecd7f4
|
||
|
||
# 热修复带回 main
|
||
git switch main
|
||
git cherry-pick 9f8e7d6
|
||
```
|
||
|
||
## 易错点
|
||
|
||
- **对公共分支 rebase 后强推**:别人的历史和你的对不上,提交会“重复”或丢失。
|
||
- **用 `--force` 而不是 `--force-with-lease`**:可能覆盖同事刚推送的提交。
|
||
- **已推送的提交用 reset 撤销**:其他人再推送时又把它带回来。用 revert。
|
||
- **revert 一个合并提交**:要用 `-m 1` 指定保留哪一边;之后想再合入同一分支,需要先 revert 这个 revert。
|
||
- **以为 reflog 能找回从未提交过的改动**:没提交(也没 stash)的工作区修改被 `reset --hard` 后是真的没了。
|
||
- **解决冲突时直接全选“我的”或“他的”**:可能丢掉对方的修改,冲突解决后要运行测试。
|
||
|
||
## 面试常问
|
||
|
||
见 [Git 面试题](interview.md):merge 和 rebase 的区别、revert 和 reset 的区别、误删提交怎么找回。
|