阶段2(下):运维专题 Docker/Nginx/Linux/CI-CD/Git 的课程、企业场景、面试题和可运行示例
This commit is contained in:
1 parent
5eb67c5d08
commit
b8010a74f3
42 files changed
+3533
-55
No files matched your search
@@ -0,0 +1,66 @@
|
||||
---
|
||||
id: devops.git.branch-commit
|
||||
title: 分支模型和提交规范:主干开发、Git Flow、Conventional Commits、代码评审
|
||||
type: lesson
|
||||
domain: devops
|
||||
stack: [git]
|
||||
tags: [分支模型, Git Flow, 主干开发, 提交规范, Code Review]
|
||||
level: basic
|
||||
est_hours: 1.5
|
||||
related: [devops.git.merge-rebase-recovery, devops.cicd.pipeline, devops.git.interview]
|
||||
week: [1, 8]
|
||||
order: 1
|
||||
---
|
||||
|
||||
# 分支模型和提交规范:主干开发、Git Flow、Conventional Commits、代码评审
|
||||
|
||||
## 解释说明
|
||||
|
||||
**分支模型**
|
||||
|
||||
| 模型 | 做法 | 适合 |
|
||||
| --- | --- | --- |
|
||||
| 主干开发(Trunk-Based) | 所有人基于 main 开短期功能分支(一两天内合并),未完成的功能用特性开关隐藏 | 有完善 CI、持续部署的团队 |
|
||||
| GitHub Flow | main 永远可发布;功能分支 → 合并请求 → 评审 → 合并 → 部署 | 大多数 Web 项目 |
|
||||
| Git Flow | `main`(发布)、`develop`(集成)、`feature/*`、`release/*`、`hotfix/*` | 有固定版本发布周期(App、客户端、私有化交付) |
|
||||
|
||||
**提交规范(Conventional Commits)**:`类型(范围): 描述`,类型常用 `feat`、`fix`、`docs`、`refactor`、`test`、`chore`、`perf`。好处:历史清晰、能自动生成变更日志、方便按类型筛选。
|
||||
|
||||
**好的提交**:一个提交只做一件事;提交信息说明“为什么”而不只是“改了什么”;不要把格式化和逻辑改动混在一起;不要提交生成文件、依赖目录和密钥(`.gitignore`)。
|
||||
|
||||
**合并请求和代码评审**:合并请求小而聚焦(几百行以内);描述写清背景、改动、测试方式;CI 通过 + 至少一人评审才能合并;main 分支开启保护,禁止直接推送和强制推送。
|
||||
|
||||
## 案例
|
||||
|
||||
本学习仓库的约定:直接在 main 上提交(个人项目),提交信息用中文描述;每个阶段完成后提交一次并推送;提交前运行 `tools/build_catalog.py --check` 和 `tools/check_sensitive.py` 检查内容规范和敏感信息。团队项目则用 GitHub Flow:`feat/xxx` 分支 → 合并请求 → CI + 评审 → squash 合并。
|
||||
|
||||
## 代码示例
|
||||
|
||||
```bash
|
||||
# 功能分支开发
|
||||
git switch -c feat/order-export
|
||||
git add -p # 按块挑选要提交的改动,避免把调试代码带进去
|
||||
git commit -m "feat(order): 支持按月导出订单" -m "运营需要按月对账,导出改为异步任务,避免接口超时"
|
||||
git push -u origin feat/order-export # 然后在平台上创建合并请求
|
||||
|
||||
# 全局忽略编辑器和系统文件
|
||||
git config --global core.excludesFile ~/.gitignore_global
|
||||
printf '.DS_Store\n.idea/\n.vscode/\n' >> ~/.gitignore_global
|
||||
|
||||
# 提交前钩子:.git/hooks/pre-commit(或用 pre-commit 框架统一管理)
|
||||
#!/bin/sh
|
||||
python3 tools/check_sensitive.py || exit 1
|
||||
```
|
||||
|
||||
## 易错点
|
||||
|
||||
- **一个提交几千行、混合多件事**:没法评审,出问题也没法单独回滚。
|
||||
- **提交信息写“update”“fix bug”**:半年后没人知道改了什么。
|
||||
- **长期不合并的功能分支**:和 main 差异越来越大,最后合并冲突无数。
|
||||
- **把 `.env`、密钥、大文件提交进仓库**:删除也会留在历史里。
|
||||
- **在公共分支上强制推送**:覆盖别人的提交。
|
||||
- **把 `node_modules`、构建产物提交**:仓库膨胀、冲突频繁。
|
||||
|
||||
## 面试常问
|
||||
|
||||
见 [Git 面试题](interview.md):你们用什么分支模型、提交规范、怎么做代码评审。
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
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 的区别、误删提交怎么找回。
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Git 练习场:在临时目录里自动演示并验证常用操作。
|
||||
|
||||
覆盖:功能分支 + 合并、rebase 整理、revert 撤销已推送的提交、reset 后用 reflog 找回、cherry-pick 热修复。
|
||||
运行:python lab.py(只需要本机安装 git,不会改动你的任何仓库)
|
||||
"""
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
out = subprocess.run(["git", *args], cwd=repo, capture_output=True, text=True,
|
||||
env={"GIT_AUTHOR_NAME": "dev", "GIT_AUTHOR_EMAIL": "dev@example.com",
|
||||
"GIT_COMMITTER_NAME": "dev", "GIT_COMMITTER_EMAIL": "dev@example.com",
|
||||
"HOME": str(repo), "PATH": "/usr/bin:/bin:/usr/local/bin"})
|
||||
if out.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(args)} 失败:{out.stderr}")
|
||||
return out.stdout.strip()
|
||||
|
||||
|
||||
def commit(repo: Path, name: str, content: str, msg: str) -> str:
|
||||
(repo / name).write_text(content)
|
||||
git(repo, "add", name)
|
||||
git(repo, "commit", "-q", "-m", msg)
|
||||
return git(repo, "rev-parse", "--short", "HEAD")
|
||||
|
||||
|
||||
def log(repo: Path, ref: str = "HEAD") -> list[str]:
|
||||
return git(repo, "log", "--format=%s", ref).splitlines()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
repo = Path(tmp)
|
||||
git(repo, "init", "-q", "-b", "main")
|
||||
commit(repo, "app.txt", "v1\n", "feat: 初始化项目")
|
||||
|
||||
# 1. 功能分支开发,期间 main 有新提交
|
||||
git(repo, "switch", "-q", "-c", "feat/login")
|
||||
commit(repo, "login.txt", "login\n", "feat(login): 登录表单")
|
||||
commit(repo, "login.txt", "login\nvalidate\n", "fix: 拼写")
|
||||
git(repo, "switch", "-q", "main")
|
||||
commit(repo, "readme.txt", "readme\n", "docs: 说明文档")
|
||||
|
||||
# 2. 在功能分支上 rebase 到最新 main:历史变成一条直线,不产生合并提交
|
||||
git(repo, "switch", "-q", "feat/login")
|
||||
git(repo, "rebase", "-q", "main")
|
||||
assert log(repo)[:3] == ["fix: 拼写", "feat(login): 登录表单", "docs: 说明文档"]
|
||||
print("rebase 后:", log(repo))
|
||||
|
||||
# 3. 合并回 main(--no-ff 保留一个合并提交,记录“这里合入了一个功能”)
|
||||
git(repo, "switch", "-q", "main")
|
||||
git(repo, "merge", "-q", "--no-ff", "feat/login", "-m", "merge: 登录功能")
|
||||
assert log(repo)[0] == "merge: 登录功能"
|
||||
|
||||
# 4. 已推送的错误提交:用 revert 生成一个反向提交,不改写历史
|
||||
bad = commit(repo, "app.txt", "v2-bug\n", "feat: 有问题的改动")
|
||||
git(repo, "revert", "--no-edit", bad)
|
||||
assert (repo / "app.txt").read_text() == "v1\n"
|
||||
assert log(repo)[0].startswith('Revert "feat: 有问题的改动"')
|
||||
print("revert 后 app.txt =", (repo / "app.txt").read_text().strip())
|
||||
|
||||
# 5. 误用 reset --hard 丢掉了提交,用 reflog 找回
|
||||
lost = commit(repo, "important.txt", "重要数据\n", "feat: 重要改动")
|
||||
git(repo, "reset", "-q", "--hard", "HEAD~1")
|
||||
assert not (repo / "important.txt").exists()
|
||||
reflog = git(repo, "reflog", "--format=%h %gs")
|
||||
found = next(line.split()[0] for line in reflog.splitlines() if "feat: 重要改动" in line)
|
||||
assert found == lost
|
||||
git(repo, "reset", "-q", "--hard", found)
|
||||
assert (repo / "important.txt").read_text() == "重要数据\n"
|
||||
print("reflog 找回提交", found)
|
||||
|
||||
# 6. 热修复:在 release 分支修复后 cherry-pick 回 main
|
||||
git(repo, "switch", "-q", "-c", "release/1.0", "HEAD~1")
|
||||
fix = commit(repo, "hotfix.txt", "fix\n", "fix: 线上紧急修复")
|
||||
git(repo, "switch", "-q", "main")
|
||||
git(repo, "cherry-pick", fix)
|
||||
assert log(repo)[0] == "fix: 线上紧急修复" and (repo / "hotfix.txt").exists()
|
||||
print("cherry-pick 后 main:", log(repo)[:2])
|
||||
print("Git 练习全部通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,12 +4,39 @@ title: Git
|
||||
type: index
|
||||
domain: devops
|
||||
stack: [git]
|
||||
tags: []
|
||||
tags: [Git, 版本控制, 分支, 协作]
|
||||
week: [1, 8, 15]
|
||||
---
|
||||
|
||||
# Git
|
||||
|
||||
分支模型、提交规范、合并与变基、误操作恢复、协作流程。
|
||||
七年前端,Git 日常操作你已经熟悉。这个专题补的是**面试和团队协作里容易被追问的部分**:分支模型怎么选、merge 和 rebase 的区别、怎么安全地撤销、误操作怎么找回、密钥误提交怎么处理。本计划的学习仓库本身也按这些规范管理。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
## 学习路线
|
||||
|
||||
| 顺序 | 课程 | 级别 | 预计 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | [分支模型和提交规范:主干开发、Git Flow、Conventional Commits、代码评审](01-branch-commit.md) | 基础 | 1.5 小时 |
|
||||
| 2 | [合并、变基和撤销:merge / rebase / cherry-pick、revert / reset、reflog 找回](02-merge-rebase-recovery.md) | 进阶 | 2 小时 |
|
||||
|
||||
## 企业场景
|
||||
|
||||
- [线上紧急修复和密钥误提交:热修复分支、cherry-pick、清理历史和轮换密钥](scenarios/hotfix-and-leaked-secret.md)
|
||||
|
||||
## 面试题
|
||||
|
||||
- [Git 面试题](interview.md)
|
||||
|
||||
## 示例代码
|
||||
|
||||
- [examples/git-lab](examples/git-lab/):`lab.py` 在临时目录里自动演示并验证 rebase、`--no-ff` 合并、revert、reflog 找回、cherry-pick
|
||||
|
||||
## 在 16 周计划中
|
||||
|
||||
- [第 1 周:仓库和提交规范](../../../plan/week01-go-gin.md)、[第 8 周:整理项目仓库](../../../plan/week08-inventory-architecture.md)、[第 15 周:作品集打包](../../../plan/week15-package-resume.md)
|
||||
|
||||
## 官方资料
|
||||
|
||||
- Pro Git(中文版):https://git-scm.com/book/zh/v2
|
||||
- Git 命令参考:https://git-scm.com/docs
|
||||
- Conventional Commits:https://www.conventionalcommits.org/zh-hans/
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
id: devops.git.interview
|
||||
title: Git 面试题
|
||||
type: interview
|
||||
domain: devops
|
||||
stack: [git]
|
||||
tags: [Git, 分支, 撤销, 面试]
|
||||
level: basic
|
||||
related: [devops.git.branch-commit, devops.git.merge-rebase-recovery]
|
||||
week: [15, 16]
|
||||
---
|
||||
|
||||
# Git 面试题
|
||||
|
||||
## 基础
|
||||
|
||||
### 【基础】`git merge` 和 `git rebase` 有什么区别?
|
||||
|
||||
**参考答案**:merge 把两条分支的历史合并,可能产生合并提交,保留真实的并行开发历史;rebase 把当前分支的提交在目标分支最新提交之后重新应用,历史变成一条直线,但会生成新的提交哈希。公共分支不要 rebase,个人功能分支可以用 rebase 同步主干、整理提交。
|
||||
|
||||
### 【基础】`git revert` 和 `git reset` 有什么区别?
|
||||
|
||||
**参考答案**:revert 生成一个新提交来抵消指定提交的改动,不改写历史,适合撤销已推送的提交;reset 移动分支指针到指定提交,`--soft` 保留改动在暂存区、`--mixed` 保留在工作区、`--hard` 丢弃改动,会改写历史,只适合未推送的本地提交。
|
||||
|
||||
### 【基础】`git fetch` 和 `git pull` 的区别?
|
||||
|
||||
**参考答案**:fetch 只下载远程的最新提交,更新远程跟踪分支(如 `origin/main`),不改动本地分支;pull = fetch + merge(或配置为 rebase),会把远程改动合并到当前分支。习惯先 fetch 看清楚差异再决定合并方式。
|
||||
|
||||
### 【基础】你们用什么分支模型?
|
||||
|
||||
**参考答案**:按真实情况回答,并说明原因。例如 GitHub Flow:main 始终可发布,功能分支通过合并请求、CI 和评审后合并,合并后自动部署测试环境。有固定版本周期的(App、私有化交付)用 Git Flow 的 release / hotfix 分支。持续部署成熟的团队用主干开发 + 特性开关。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】误执行了 `git reset --hard`,提交还能找回吗?
|
||||
|
||||
**参考答案**:只要曾经提交过就能找回。`git reflog` 记录了 HEAD 的每次移动,找到 reset 之前的提交哈希,`git reset --hard 哈希` 或 `git branch 新分支 哈希` 恢复。删除的分支同理。但从未提交、也没有 stash 的工作区改动无法通过 reflog 找回。
|
||||
|
||||
### 【进阶】`--force` 和 `--force-with-lease` 有什么区别?
|
||||
|
||||
**参考答案**:`--force` 无条件用本地覆盖远程分支;`--force-with-lease` 只有当远程分支仍然是你上次获取时的状态才覆盖,如果别人在这期间推送了新提交就会拒绝,避免误覆盖同事的工作。改写过历史的个人分支推送时应该用后者。
|
||||
|
||||
### 【进阶】什么时候用 cherry-pick?有什么注意事项?
|
||||
|
||||
**参考答案**:需要把某个提交单独应用到另一个分支时,例如热修复同时带到 release 和 main、把某个修复回移到旧版本。注意 cherry-pick 会生成新的提交(不同哈希),如果之后两个分支合并,可能出现重复改动或冲突;大量 cherry-pick 说明分支策略可能有问题。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】有人把密钥提交到了远程仓库,怎么处理?
|
||||
|
||||
**参考答案**:第一步立即作废并轮换密钥、检查是否被滥用;然后删除文件并加入 `.gitignore`;视情况用 `git filter-repo` 清理历史并强制推送,通知所有人重新克隆,联系平台清理缓存;最后加上提交前和 CI 中的密钥扫描。清理历史不能代替轮换。详见[热修复和密钥误提交场景](scenarios/hotfix-and-leaked-secret.md)。
|
||||
|
||||
### 【场景题】线上版本有紧急 bug,但 main 上已经有很多未发布的功能,怎么修?
|
||||
|
||||
**参考答案**:从线上版本的标签或 release 分支拉热修复分支,修复并补测试,走 CI 后合入 release 分支发布补丁版本;再把修复 cherry-pick 或合并回 main,保证下个版本包含修复。不要从 main 拉分支修复,否则会把未测试的功能带上线。
|
||||
|
||||
### 【场景题】合并时遇到大量冲突,怎么处理?
|
||||
|
||||
**参考答案**:先理解双方各自改了什么(`git log`、`git diff` 看两边的提交),逐个文件解决,不要一键全选某一方;解决后运行测试;冲突太多时可以和对方一起解决。根本上要避免长期不合并的分支:功能分支保持短小、经常同步主干。
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
id: devops.git.scenario.hotfix-and-leaked-secret
|
||||
title: 线上紧急修复和密钥误提交:热修复分支、cherry-pick、清理历史和轮换密钥
|
||||
type: scenario
|
||||
domain: devops
|
||||
stack: [git, cicd]
|
||||
tags: [热修复, cherry-pick, 密钥泄露, 历史清理, 安全]
|
||||
level: intermediate
|
||||
prerequisites: [devops.git.merge-rebase-recovery]
|
||||
est_hours: 1.5
|
||||
related: [devops.git.interview, devops.cicd.pipeline]
|
||||
week: [8, 15]
|
||||
---
|
||||
|
||||
# 线上紧急修复和密钥误提交:热修复分支、cherry-pick、清理历史和轮换密钥
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
团队按“release 分支发版”的方式工作:`main` 上已经合入了下个版本的多个功能(未测试完),线上运行的是 `release/1.4`。周五下午出现两件事:
|
||||
|
||||
1. 线上 1.4 版本有一个金额计算的 bug,需要马上修复,但不能把 main 上未测试的功能带上线。
|
||||
2. 有同事把含有大模型 API Key 的 `.env` 文件提交并推送到了远程仓库。
|
||||
|
||||
## 约束
|
||||
|
||||
- 热修复要在一小时内上线,而且下个版本也必须包含这个修复。
|
||||
- 仓库在公司内部平台上,有十几个人已经拉取过包含密钥的提交。
|
||||
|
||||
## 方案设计
|
||||
|
||||
**热修复**
|
||||
|
||||
1. 从线上版本的标签 / release 分支创建 `hotfix/amount-rounding`;
|
||||
2. 修复 + 补测试,走合并请求和 CI,合入 `release/1.4`,打标签 `v1.4.1` 发布;
|
||||
3. 把修复提交 `cherry-pick` 到 main(或从 release 合并回 main),保证下个版本不会“复活”这个 bug。
|
||||
|
||||
**密钥泄露**——顺序很重要:
|
||||
|
||||
1. **第一时间轮换密钥**:在模型服务商控制台作废旧 Key、生成新 Key,并更新部署环境的密钥配置。只要密钥进过远程仓库,就必须当作已经泄露,**清理历史不能代替轮换**。
|
||||
2. 检查旧 Key 的调用记录,确认是否有异常使用。
|
||||
3. 删除文件并加入 `.gitignore`;提供 `.env.example`。
|
||||
4. 评估是否需要清理历史:用 `git filter-repo` 从所有历史中移除文件,强制推送,通知所有人重新克隆;托管平台上的缓存、合并请求、fork 中可能仍有副本,需要联系平台管理员。
|
||||
5. 预防:提交前钩子扫描密钥(本仓库的 `tools/check_sensitive.py`),CI 中也执行密钥扫描。
|
||||
|
||||
## 关键代码
|
||||
|
||||
```bash
|
||||
# 热修复
|
||||
git switch -c hotfix/amount-rounding v1.4.0
|
||||
# ……修改代码、补测试……
|
||||
git commit -am "fix(order): 金额四舍五入改为银行家舍入前先转换为分"
|
||||
# 合并请求合入 release/1.4 后打标签
|
||||
git tag -a v1.4.1 -m "热修复:金额计算"
|
||||
git push origin v1.4.1
|
||||
# 带回 main
|
||||
git switch main && git cherry-pick <修复提交哈希>
|
||||
|
||||
# 密钥:先轮换!然后再处理仓库
|
||||
git rm --cached .env
|
||||
echo ".env" >> .gitignore
|
||||
git commit -m "chore: 移除误提交的 .env,并忽略该文件"
|
||||
# 需要清理历史时(会改写所有提交哈希,需要团队协调):
|
||||
git filter-repo --path .env --invert-paths
|
||||
git push --force --all && git push --force --tags
|
||||
```
|
||||
|
||||
`git filter-repo` 需要单独安装,用法以官方说明为准:https://github.com/newren/git-filter-repo 。在临时仓库里练习 cherry-pick、revert、reflog 的脚本见 [examples/git-lab](../examples/git-lab/)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 做法 | 优点 | 缺点 |
|
||||
| --- | --- | --- |
|
||||
| **只轮换密钥 + 删除文件** | 快、不影响团队 | 旧密钥仍留在历史里(已作废,风险可控) |
|
||||
| 轮换 + 清理历史 | 历史中不再有密钥 | 所有提交哈希改变,所有人要重新克隆;平台副本仍需单独处理 |
|
||||
| 只清理历史不轮换 | —— | **错误做法**:密钥可能已被复制 |
|
||||
|
||||
热修复用 cherry-pick 还是“release 合并回 main”:cherry-pick 精确,只带修复;合并回 main 能保证 release 上所有改动都回到 main,但可能带来冲突。团队约定一种即可。
|
||||
|
||||
## 易错点
|
||||
|
||||
- 热修复只改了 release 分支,忘了带回 main:下个版本 bug 又出现。
|
||||
- 从 main 拉热修复分支:把未测试的功能带上线。
|
||||
- 以为“删除文件再提交一次”密钥就安全了:历史里仍然有。
|
||||
- 先花半天清理历史再轮换密钥:轮换应该是第一步。
|
||||
- 强制推送清理后的历史却没通知团队:有人基于旧历史推送,又把文件带回来。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “线上版本出 bug 时,我从线上的标签拉热修复分支,修复并补测试后合入 release 分支打补丁版本发布,再 cherry-pick 回 main,避免把 main 上未测试的功能带上线。还处理过密钥误提交:第一步是作废并轮换密钥,检查调用记录;然后删除文件、加入 .gitignore,加上提交前的密钥扫描。是否清理历史要评估成本,清理时用 filter-repo 并通知团队重新克隆。”
|
||||
Reference in new issue
Block a user