知识广场

按学科筛选:计算机科学 / 软件工程 / 版本控制
清除筛选

«计算机科学 / 软件工程 / 版本控制» 分类下共 7 篇帖子

git rebase -i:把零散提交压成一条 + 改提交信息 + 拆提交

写代码时 commit 颗粒度往往很碎:"修了个 typo"、"WIP"、"忘了 import"。 推到主分支前用 `rebase -i` 重写历史,让 PR 干净。 ## 1. 启动 ```bash git rebase -i HEAD~5 # 重写最近 5 个提交 # 或: git rebase -i origin/main # 重写从 main 分叉以来的所有提交 ``` 打开编辑器: ``` pick a1b2c3d Add user model pick e4f5g6h Add tests pick i7j8k9l fix typo pick m1n2o3p more tests pick q5r6s7t WIP # Rebase 1234567..q5r6s7t onto 1234567 (5 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log # d, drop = remove commit ``` ## 2. 最常用:squash + 改 message ``` pick a1b2c3d Add user model fixup e4f5g6h Add tests # 合到上一条,丢弃 message fixup i7j8k9l fix typo pick m1n2o3p more tests fixup q5r6s7t WIP ``` 保存退出。Git 把这 5 个提交重写成 2 个: - 第 1 个:a1b2c3d 的 message + 包含 a/e/i 的代码 - 第 2 个:m1n2o3p 的 message + 包含 m/q 的代码 `fixup` vs `squash`:squash 让你重新写合并后的 message,fixup 直接丢弃。 有改 message 需求用 squash,没需求用 fixup。 ## 3. 调整顺序 直接把行换位置即可: ``` pick m1n2o3p more tests # 原本第 4 个 pick a1b2c3d Add user model # 原本第 1 个 ... ``` Git 会按新顺序重新 cherry-pick。换序时如果两提交改动了同样的文件, 可能 conflict —— 解决然后 `git rebase --continue`。 ## 4. 拆一个提交 想把一个"什么都改了"的大提交拆成几个小的: ``` edit a1b2c3d Big mess of changes ``` 保存后 git 停在那个提交,你可以: ```bash git reset HEAD~ # 把改动放回工作区 git add file1.py git commit -m 'add file1' git add file2.py git commit -m 'add file2' git rebase --continue ``` ## 5. 改某个提交的内容 发现某个旧提交少改一行: ``` edit e4f5g6h Add tests ``` 停下来后: ```bash # 改你要改的文件 vim test_user.py git add test_user.py git commit --amend --no-edit git rebase --continue ``` ## 6. autosquash:标记 fixup 提交 写代码时直接打 fixup 标签: ```bash git commit --fixup=a1b2c3d # 标记这是给 a1b2c3d 的修补 # 后续可以多个 fixup 提交 git commit --fixup=a1b2c3d git commit --fixup=m1n2o3p ``` 然后: ```bash git rebase -i --autosquash origin/main ``` `--autosquash` 自动把每个 `fixup!` 提交移到对应原提交后面,并标 `fixup`。 你只需要确认 + 保存。这是我个人 99% 的 rebase 用法。 可以让 autosquash 默认开: ```bash git config --global rebase.autosquash true ``` ## 7. 安全推 rebase 后本地历史和远端不一致,普通 push 会被拒绝。要 force push: ```bash git push --force-with-lease origin feature-branch ``` `--force-with-lease` 比 `--force` 安全:如果远端有别人新推的提交(你不知道), 会拒绝。`--force` 会盲目覆盖,可能毁掉同事的工作。 ## 8. 出错回滚 rebase 把事情搞乱了? ```bash git rebase --abort # 进行中的 rebase 直接放弃 git reflog # 看历史所有 HEAD 移动 git reset --hard HEAD@{5} # 回到 5 步前的状态 ``` reflog 是 Git 的 undo 神器,rebase 出错 90% 都能从这里救回来。 默认保留 90 天。 ## 9. 不要 rebase 的场景 - **已经推到共享分支**(main / develop)的提交:rebase 后 force push 会让其他人 pull 时混乱 - **多人共同开发的 feature 分支**:同理 - 安全的规则:"只 rebase 自己的本地分支或单人 feature 分支" ## 10. merge --squash 的替代方案 如果 feature 分支提交特别乱,懒得 rebase -i,直接: ```bash git checkout main git merge --squash feature-branch git commit -m 'feat: add user system' ``` 会把整个 feature 分支压成一个 staged 改动,由你写一条 commit message。 这是"合并时清整历史"的快捷做法,但失去了原始提交的颗粒度。 ## 踩过的坑 - rebase 时 conflict 改完忘记 `git add` → `git rebase --continue` 报错 "no changes"。 - 用 GUI 工具做 rebase:很多 GUI 让操作太容易,新手秒搞砸主分支。 建议命令行做 rebase,至少有一道心理门槛。 - `git push --force` 主分支:CI 测试基于旧 SHA 的 deploy 全失效, 其他 dev 拉不下来。绝对禁止,用 GitHub branch protection 锁住。 - rebase 改 message 时含中文,OS / 编辑器编码不一致会变乱码。 git config `i18n.commitEncoding utf-8`。

团队 commit 信息一团乱?conventional commits + commitlint + husky 强制规范

## 起因 我们小团队 5 个人维护一个 monorepo,半年下来 git log 一塌糊涂: `fix bug` / `WIP` / `更新一下` / `aaaaa`。每周一次的 release notes 得人工梳理,找哪个 commit 是新功能、哪个是 bug fix,每次半小时起步。 ## 解决方案:conventional commits + 工具链强制 约定 commit 必须以类型开头: ``` feat: 新功能 fix: bug 修复 docs: 文档 style: 格式化(不影响功能) refactor: 重构 perf: 性能优化 test: 测试 chore: 构建 / CI / 依赖 revert: 回滚 ``` 可选 scope:`feat(auth): add OAuth`、`fix(api): correct status code`。 breaking change 加 `!`:`feat!: drop Node 16 support`。 ### 装 commitlint + husky ```bash npm i -D @commitlint/cli @commitlint/config-conventional husky # 初始化 husky npx husky init # commitlint 配置 echo "export default { extends: ['@commitlint/config-conventional'] }" \ > commitlint.config.js # git hook echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg chmod +x .husky/commit-msg ``` 之后任何不合规范的 commit 直接被拒: ``` $ git commit -m "update stuff" ✖ subject may not be empty ✖ type may not be empty ✖ found 2 problems, 0 warnings ``` ### 配合 commitizen 让团队上手 ```bash npm i -D commitizen cz-conventional-changelog ``` `package.json` 加: ```json { "scripts": { "commit": "cz" }, "config": { "commitizen": { "path": "cz-conventional-changelog" } } } ``` `npm run commit` → 交互式选 type / scope / subject,新人 30 秒上手。 ### 自动生成 changelog ```bash npm i -D conventional-changelog-cli npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0 ``` 把所有 commit 按 type 分类、按 scope 分组、生成 markdown changelog。 更进一步用 semantic-release:基于 commit type 自动 bump 版本号 + 生成 release notes + 发到 npm + 创建 GitHub release: ```bash npm i -D semantic-release ``` `.github/workflows/release.yml`: ```yaml - run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ``` merge 到 main → CI 看到 `feat:` 自动发 minor 版本,`fix:` 发 patch, `feat!:` 发 major。完全无人值守。 ## 效果 - 一周后所有人习惯了;commit log 整洁可读 - 每月 release notes 从 30 分钟人工梳理 → 0 分钟自动生成 - PR review 时只看 commit 标题就知道改动性质 - 半年后回头看 git log,能复盘"上次的 OAuth feature 是哪天合的" ## 踩过的坑 1. **第一次接入老仓库 husky 不工作**:`.husky/` 目录权限不对,hook 文件 要可执行(`chmod +x`)。CI 里 `npm ci` 后 husky install 有时不触发, `prepare` script 设 `husky` 保险。 2. **commitizen 卡在 monorepo**:subpath 项目里 commitizen 找不到配置。 根目录 .czrc 用绝对路径或者全局装 commitizen-cli。 3. **rebase 时 commit-msg hook 反复触发**:rebase 多个老 commit 都 过一遍 lint,老的破格式都 fail。一次性 `HUSKY=0 git rebase ...` 绕过,整理完后再开。 4. **scope 不要太多**:定义 5-7 个核心 scope 就好(如 `auth` / `api` / `ui` / `db` / `infra`)。每个目录一个 scope 反而没用,commit 信息 被 scope 撑长。 5. **breaking change 容易忘**:仅靠 `!` 易遗漏。配合 PR template 让 提 PR 时勾"是否含 breaking" 列表,CI lint 时校验 commit footer 有 `BREAKING CHANGE:` 描述。

git worktree:同时开多个分支不切换、不 stash、不 clone

写到一半 feature 分支,突然 main 上有紧急 bug 要修。传统流程: 1. `git stash` 2. `git checkout main && git checkout -b hotfix` 3. 修完合回 4. `git checkout feature && git stash pop` 容易出错(stash 冲突、忘了某些 untracked 文件)。 `git worktree` 让你在同一个仓库下挂多个 working directory,每个独立 checkout 一个分支。 ## 1. 加一个 worktree ```bash cd ~/repos/myapp # 你正在 feature-x 分支上 git worktree add ../myapp-hotfix main # Preparing worktree (new branch 'hotfix' from 'main') # HEAD is now at a1b2c3d Latest main commit ``` 现在 `~/repos/myapp-hotfix/` 是一个完整的工作目录,checkout 在 main 上: ```bash cd ~/repos/myapp-hotfix git status # On branch main # nothing to commit, working tree clean ``` 你的原目录 `~/repos/myapp/` 还在 feature-x 上,没动。 修完 hotfix: ```bash cd ~/repos/myapp-hotfix git checkout -b hotfix-cve-1234 # 改代码 git commit -am 'fix: CVE-1234' git push -u origin hotfix-cve-1234 # 开 PR / 合并 ``` 完事删 worktree: ```bash cd ~/repos/myapp git worktree remove ../myapp-hotfix ``` ## 2. 列出所有 worktree ```bash git worktree list # /home/me/repos/myapp a1b2c3d [feature-x] # /home/me/repos/myapp-hotfix e4f5g6h [hotfix-cve-1234] # /home/me/repos/myapp-review i7j8k9l [pr-42-review] ``` ## 3. 常用 pattern ### A. PR review 不打断当前工作 ```bash git worktree add ../myapp-pr42 origin/feature-from-coworker cd ../myapp-pr42 # review、跑测试、不干扰主工作目录 ``` ### B. 每个项目固定一个 main worktree ```bash ~/repos/myapp/ # 永远 main,用于 git pull / 看 README ~/repos/myapp-dev/ # feature 分支工作目录 ~/repos/myapp-pr/ # 任意 review 用 ``` ### C. 用于 git bisect bisect 期间不能用工作目录干别的。开 worktree 让 bisect 单独跑, 你继续工作: ```bash git worktree add ../myapp-bisect HEAD cd ../myapp-bisect git bisect start git bisect good v1.0 git bisect bad HEAD # bisect 在这个 worktree 来回 checkout,不影响主工作目录 ``` ## 4. 用 alias 加速 ```bash git config --global alias.wta 'worktree add' git config --global alias.wtl 'worktree list' git config --global alias.wtr 'worktree remove' ``` 之后: ```bash git wta ../hotfix main git wtl git wtr ../hotfix ``` ## 5. 替代 multiple clone 老方法是 `git clone` 同一个 repo 多份: - 磁盘占用 N 倍 - 不能跨工作目录复用 stash / config / hooks - 拉新 commit 要在每个 clone 上 pull worktree 共享同一个 `.git/`,所有 worktree 共享对象数据库, 拉一次所有 worktree 立即可见。 ## 6. detached HEAD worktree(无分支) 只想 checkout 某个 tag / commit 看看: ```bash git worktree add ../myapp-v1.0 v1.0 # 自动 detached HEAD ``` 完了 `git worktree remove ../myapp-v1.0` 就清掉。 ## 7. 子目录 + IDE VSCode / IntelliJ 各开一个窗口指到不同 worktree,能同时调试多个分支 而不互相干扰。 ## 8. CI worktree CI 想在同一 runner 上并发跑多个分支的测试: ```bash git worktree add /tmp/test-pr-42 origin/pr-42 & git worktree add /tmp/test-pr-43 origin/pr-43 & wait # 然后并行 cd 进去跑测试 ``` 避免反复 clone 的开销。 ## 9. lock / unlock worktree 在外置磁盘 / 网络盘上时,磁盘没挂载 worktree 会被 "automatic prune"。给它加锁防止: ```bash git worktree lock ../myapp-external --reason 'on external SSD' git worktree unlock ../myapp-external ``` ## 10. prune worktree 目录被手动 rm 但 `.git/worktrees/` 里还有元数据: ```bash git worktree prune # 清理孤立元数据 ``` ## 踩过的坑 - 同一个分支不能同时 checkout 到两个 worktree(git 会拒绝)。需要的话 用 `git worktree add --force` 或者 detached HEAD 指到分支 tip。 - worktree 在 NTFS / FAT 上:`.git/worktrees/<name>/gitdir` 的路径用了 绝对路径,跨平台 / 移动后失效。Linux 用就好。 - 在 worktree 里删了文件然后 `git stash`:stash 是 repo 级别的,主 worktree 也能看到这个 stash。共享 stash 偶尔混淆,建议 stash 加 message 区分。 - 切换 IDE 不会自动重读 worktree —— 关掉 IDE 进程重新打开, 否则 IDE 缓存的 git 状态可能错。

终端开 PR / review / merge:gh CLI 让我每天少切 20 次浏览器

## 起因 写完代码 → 推分支 → 打开浏览器 → 找到仓库 → 点 "Compare & pull request" → 填标题描述 → 加 reviewer → 等审 → 收通知 → 看评论 → 回评论 → 改代码 → 重复。一天来回浏览器和编辑器几十次,每次都打断 flow。 `gh` 是 GitHub 官方 CLI,把 PR 的整个生命周期搬到终端。 ## 解决方案 ### 装 + 登录 ```bash # macOS / Linux brew install gh # 或 Debian/Ubuntu sudo apt install gh gh auth login # 选 GitHub.com → HTTPS → 浏览器登录 ``` ### 开 PR ```bash # 推完分支后一行开 PR gh pr create --base main --title "feat: add OAuth" \ --body "Closes #42. ... ## Test plan - [ ] manual login - [ ] CI green" # 或者完全交互式 gh pr create # 自动用最近的 commit 信息预填 ``` 更便利的: ```bash # 创建 + 自动指 reviewer + draft gh pr create --draft --reviewer alice,bob,@org/backend ``` ### 看 / review / 合 ```bash # 看本仓库所有 open PR gh pr list gh pr list --author @me gh pr list --label bug # 看某 PR 详情 gh pr view 123 gh pr view 123 --web # 还是想浏览器看 # 看 diff gh pr diff 123 gh pr diff 123 --color always | less -R # checkout 这个 PR 到本地 gh pr checkout 123 # Review gh pr review 123 --approve --body "LGTM" gh pr review 123 --comment --body "some notes" gh pr review 123 --request-changes --body "..." # 看 CI 状态 gh pr checks 123 # 合并(squash + 删分支) gh pr merge 123 --squash --delete-branch # admin override (绕过 required reviews,谨慎) gh pr merge 123 --admin --squash ``` ### 看自己/团队的 PR backlog 写个 alias: ```bash alias my-prs='gh pr list --author @me --state all --limit 20' alias to-review='gh pr list --search "review-requested:@me"' alias team-prs='gh pr list --search "org:my-org is:open"' ``` `to-review` 是我每天上班第一件事 —— 一行看到所有等我 review 的 PR。 ### issue 也一并管 ```bash gh issue list --assignee @me gh issue create --title "..." --body "..." --label bug --assignee @me gh issue close 42 --comment "fixed in PR #43" ``` ### CI / workflow ```bash # 看 actions run gh run list --workflow=test.yml gh run view 8123456789 gh run watch # 等当前分支最新 run 跑完,CI 通过通知 # 触发手动 workflow gh workflow run deploy.yml -f environment=prod ``` `gh run watch` 是 release 时神器:push 后跑一行,CI 跑完自动桌面通知, 不必去浏览器盯。 ### 自动复制 PR 链接 ```bash gh pr view 123 --json url --jq .url | pbcopy # macOS gh pr view 123 --json url --jq .url | xclip # Linux ``` 写到团队 Slack 时一秒粘上 URL。 ### gh extensions ```bash gh extension install dlvhdr/gh-dash # TUI 仪表盘 gh extension install vilmibm/gh-screensaver # ASCII 烟花(彩蛋) gh extension install yusukebe/gh-markdown-preview ``` `gh dash` 是装了之后再也戒不掉的 TUI:一屏看自己的 PR / 待 review / 最近 issue,j/k 浏览,o 在浏览器打开。 ## 效果 - 开 PR 平均 < 30 秒(之前要 2-3 分钟) - review 流程不切上下文:`gh pr diff 123` + `gh pr review --approve` - CI 状态 `gh pr checks` 比刷网页快 - 每天浏览器切换次数下降 70%+ ## 踩过的坑 1. **token 权限不够**:`gh auth status` 看 scope。如果要管 GitHub Actions / Pages 等需要额外 scope,`gh auth refresh -s admin:org` 补权限。 2. **多账号**:个人 + 公司 GitHub 都用 gh,`gh auth switch` 切。 `--user` flag 也能临时指定。 3. **company GitHub Enterprise**:`gh auth login --hostname github.company.com` 单独登录;命令默认走该 host。 4. **gh pr create 不显示 default branch**:仓库设置 default branch 没 配,gh 默认拿你 push 的 branch。先 `gh repo edit --default-branch main`。 5. **`gh pr checkout` 不能 fetch 别的 fork 的 PR 时**:在公开 fork 的 PR 上 有时 git 配置不对。手动 `git fetch upstream pull/123/head:pr-123` 然后 `git checkout pr-123`。

lazygit:终端里把 git 用得比 IDE 快的 TUI

## 起因 我每天 `git status` / `git add -p` / `git commit -m` / `git push` / `git log --oneline` / `git diff HEAD` 重复几十次。打字成本不算高但 每次都要看一眼输出再决定下一步,rhythm 容易卡。IDE 的 git 面板鼠标 操作又比键盘慢。 `lazygit` 是 Go 写的 git TUI:一屏看到 status / branches / log / stash / commits / files,全键盘操作。 ## 安装 ```bash # Debian / Ubuntu (PPA) LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" \ | grep -oE '"tag_name":\s*"v[^"]+"' | sed 's/.*"v\([^"]*\)"/\1/') curl -Lo lazygit.tar.gz \ "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" tar xf lazygit.tar.gz lazygit sudo install lazygit /usr/local/bin rm lazygit.tar.gz lazygit # macOS brew install lazygit # Windows scoop install lazygit ``` 在任意 git 仓库里跑: ```bash lazygit # 或者起个 alias echo 'alias lg=lazygit' >> ~/.bashrc ``` ## 关键键位 主屏分 5 个 panel:files / branches / commits / stash / status。 `Tab` / `[` `]` 切 panel。 ### Files panel - `<space>` stage / unstage(按整文件) - `<enter>` 进入文件 → 选行 / hunk → `<space>` 部分 stage - `d` discard(清除改动) - `c` commit(弹起编辑器写 message) - `C` commit without verifying (--no-verify) - `a` stage all - `i` add to .gitignore ### Branches panel - `<space>` checkout - `n` 新建分支 - `d` 删除 - `r` rebase 选中分支到当前 - `f` fast-forward 拉这条分支(不切过去) - `M` merge 进当前分支 ### Commits panel - `<space>` checkout 这个 commit - `c` cherry-pick - `r` reword commit message - `e` interactive edit (相当于 rebase -i + edit) - `f` fixup 上一条 - `s` squash 进上一条 - `d` drop - `<enter>` 看 commit diff,再选文件 / hunk 看具体改动 ### Stash panel - `s` stash 当前改动 - `<space>` apply - `g` pop(apply + drop) - `d` drop ### Status panel (左上) - `,` switch to recent repos - 看 ahead/behind 远端的数量 ## 一些日常操作的"光速" gesture ### 局部 stage(hunk-level) ``` 进 files → enter 进文件 → ↓↑ 选 hunk → <space> stage 这块 → q 退出 → c commit ``` 比 `git add -p` 流畅 5x。 ### Interactive rebase 一气呵成 ``` commits panel → e(edit)→ 选要 reword 的 → r → 改 message → 选要 fixup 的 → f → m menu → 'continue rebase' ``` 不用记 `git rebase -i HEAD~5` 的所有 squash/reword/edit/drop 单字母。 ### 解 conflict merge / rebase 冲突时 lazygit 自动跳到 conflict 面板: ``` ↓↑ 选要保留的版本 (ours / theirs / both) → <space> 选 → 下一处 ``` 复杂 conflict 仍要进编辑器手解,但简单的可以 lazygit 内完成。 ### 自动 push / fetch ``` P push p pull f fetch ``` `shift+P` 强制推(force push with lease)。 ## 自定义命令 `~/.config/lazygit/config.yml`: ```yaml gui: showFileTree: true # 文件树视图 showRandomTip: false language: 'auto' scrollHeight: 2 customCommands: - key: 'C' context: 'files' command: "git commit -m '{{.Form.Message}}' --signoff" prompts: - type: 'input' title: 'commit message (with --signoff)' key: 'Message' - key: 'b' context: 'commits' description: 'git bisect from this commit' command: "git bisect start && git bisect bad HEAD && git bisect good {{.SelectedLocalCommit.Sha}}" ``` ## 与 nvim / vscode 集成 ```bash # 在 vim 里直接打开 lazygit nnoremap <leader>g :LazyGit<CR> # 需要装 kdheepak/lazygit.nvim ``` VSCode 装 `lazygit-vscode` 扩展,`Cmd+Shift+P → LazyGit` 启动。 ## 效果 - daily 流程从 "切窗口 → 打命令 → 看输出 → 想下一步" 变成 "lg → 几个 字母 → 完成" - 复杂的 rebase / cherry-pick / branch 切换不再让我犹豫"忘了命令" - 同事被我安利后我推荐过的 5 个人有 4 个换成了 lazygit - 老 git 命令照样会用(脚本里、CI 里),lazygit 是 interactive 场景的 覆盖 ## 踩过的坑 1. **大仓库(10w+ files)启动慢**:lazygit 会扫整个 status。在 monorepo 里设 `git config core.untrackedCache true` + `core.fsmonitor true` 提速。 2. **conflict UI 不显示 conflict marker 颜色**:终端不支持 truecolor。 换 iTerm2 / Alacritty / WezTerm 解决。 3. **快捷键和 vim 冲突**:把 `:` 作为 lazygit menu 的话和 vim 命令模式 冲突。`x` `:` 改 keybinding。 4. **跨平台行尾问题**:Windows + Linux 协作时 lazygit 显示一堆 "modified no diff",是 CRLF 自动转换。`git config core.autocrlf` 团队统一。 5. **git LFS**:lazygit 不知道 LFS 文件,stage 大文件可能不走 LFS pointer。 先 `git lfs track` 配好 .gitattributes。

git bisect:二分定位引入 bug 的提交(含自动化)

某次合并后线上崩了,可疑提交几十上百个,一个个 checkout 太慢。 `git bisect` 做二分搜索:log N 次 checkout 就找出元凶。 ## 基本流程 ```bash git bisect start git bisect bad HEAD # 现在的状态是坏的 git bisect good v1.5.0 # 这个 tag 是好的 # Git checkout 中间提交,让你测: # Bisecting: 38 revisions left to test after this (roughly 6 steps) # [a1b2c3d] Some commit ``` 你测一下(手动或脚本): ```bash # 如果坏: git bisect bad # 如果好: git bisect good # 测不了(编译失败 / 不相关): git bisect skip ``` Git 自动 checkout 下一个二分点。重复直到: ``` # a1b2c3d is the first bad commit # commit a1b2c3d # Author: ... # Date: ... # feat: refactor user permission check ``` 完事: ```bash git bisect reset # 回到原本的 HEAD ``` ## 自动化(最好用的方式) 写一个返回 0 / 1 的检查脚本: ```bash # check.sh #!/usr/bin/env bash set -e make build > /dev/null 2>&1 || exit 125 # 编译失败 → skip ./run_test.sh > /dev/null 2>&1 # exit 0 = good, !=0 = bad ``` 退出码约定: - `0`:good - `1` ~ `124`、`126` ~ `127`:bad - `125`:skip - `128+`:abort bisect 然后: ```bash git bisect start HEAD v1.5.0 git bisect run ./check.sh ``` Git 自动跑完所有二分步骤,结束时打印 first bad commit。 20 个提交需要约 5 次 checkout + 测试。 ## 真实例子:找性能回归 线上某 endpoint 从 50ms 涨到 200ms: ```bash # benchmark.sh #!/usr/bin/env bash make build ./serve & PID=$! sleep 2 LAT=$(curl -s -o /dev/null -w '%{time_total}' http://localhost:8000/api/x) kill $PID # 50ms 以下算好 awk "BEGIN { exit ($LAT < 0.10) ? 0 : 1 }" ``` ```bash git bisect start HEAD v1.5.0 git bisect run ./benchmark.sh ``` 20 分钟后告诉你是哪个提交把性能拖坏的。 ## 一次同时盯多个文件 bisect 时如果你只想看某子目录: ```bash git bisect start --term-good=fast --term-bad=slow -- src/api/ ``` `--` 后面限定路径范围;只把改了 `src/api/` 的提交纳入二分。 搜索空间小很多。 ## skip 多个提交 某段提交都 build 失败(比如有人合错了破坏分支),全 skip: ```bash git bisect skip $(git rev-list bad-broken^..good-broken) ``` bisect 算法会跳过这段继续。 ## 可视化 bisect 当前状态 ```bash git bisect log # git bisect start # # bad: [abcdef] description # git bisect bad abcdef # # good: [123456] description # git bisect good 123456 # # skip: [xyzxyz] ... # git bisect skip xyzxyz ``` 把 log 输出存文件,回头能重放 bisect: ```bash git bisect log > /tmp/bisect.txt git bisect reset # ... 时间过去 ... git bisect replay /tmp/bisect.txt # 还原到之前的进度 ``` ## 与 git worktree 配合 bisect 期间不能在主目录干别的。开个 worktree 让 bisect 单独跑: ```bash git worktree add ../bisect-tmp HEAD cd ../bisect-tmp git bisect start HEAD v1.5.0 git bisect run ./check.sh # 完事 cd ../myapp git worktree remove ../bisect-tmp ``` ## 不止 commit —— bisect tag / branch ```bash git bisect start v2.0 v1.0 # 在两个 tag 之间二分 git bisect start origin/main origin/main~100 # 最近 100 个 commit ``` ## 几个手动 bisect 的小 trick ### 中途调整范围 ```bash git bisect bad <some-commit> # 把 "bad" 边界从 HEAD 改到更早 git bisect good <other> # 把 "good" 边界改到更晚 ``` bisect 重新计算二分。 ### 临时 fix forward 后继续 某中间 commit build 失败需要小补丁才能跑你的 check: ```bash # 在二分到的 commit 上 git cherry-pick <fix-commit> # 临时打补丁 ./check.sh git bisect good/bad # 根据结果 git reset --hard HEAD~ # 撤销 cherry-pick git bisect <next> ``` ## 踩过的坑 - bisect 用的 check 脚本不稳定(偶尔 false bad / false good)→ 二分到 错误结论。check 脚本必须 deterministic。 - `git bisect bad` 漏写 commit 参数 → 自动用 HEAD(当前 bisect 选的) 当 bad。手抖容易标错。 - bisect 跨越了 merge commit,结果指到一个 merge commit。看 `git log --first-parent` 复审;真正 bug 在 merge 的某个 parent 分支里, 对那个分支再做二级 bisect。 - 整套流程用 `git bisect run` 自动化才省力。手动 100 次每次手测, 错一次就要 reset 重来。

Git 子项目共享代码:submodule / subtree / monorepo 怎么选

## 起因 我们有 5 个微服务都用一份"共享 utility 代码"(如 protobuf 定义 / 通用 model / shared logger)。三种主流方式: A. 各项目 copy-paste(同步靠人工,最差) B. 抽成 npm/pip 包发包(增加发版流程) C. Git submodule 共享一个 repo D. Git subtree 把共享 repo 嵌入主 repo E. monorepo 把所有项目放一起 各自适用场景不同。 ## A. Git submodule ```bash cd myservice git submodule add https://github.com/myorg/shared-libs.git lib/shared git commit -m 'add shared submodule' ``` `.gitmodules` 文件 + lib/shared 目录(指向 shared-libs 的某个 commit)。 ```bash # clone 主 repo 时要 init submodule git clone --recurse-submodules https://github.com/myorg/myservice.git # 或事后 cd myservice git submodule update --init --recursive ``` 更新 submodule: ```bash cd lib/shared git pull origin main cd ../.. git add lib/shared git commit -m 'bump shared submodule to abc123' ``` 主 repo 记录 "我指向 shared 的哪个 commit"。 **优点**: - 共享 repo 独立版本控制 - 每个项目可固定某个版本(不同步升级) **缺点**: - 命令繁琐(`git submodule update` 经常忘) - 新成员第一次 clone 总踩坑(忘 `--recurse-submodules`) - 改 submodule 内代码后要 push 共享 repo + 更新主 repo 引用 = 两个 PR ## B. Git subtree ```bash cd myservice git subtree add --prefix=lib/shared https://github.com/myorg/shared-libs.git main --squash ``` 效果:把 shared-libs 整个内容 copy 到 lib/shared/ 目录,作为本 repo 的真实 commit(不是引用)。 ```bash # 拉 shared 上游更新 git subtree pull --prefix=lib/shared shared-remote main --squash # 把本地 shared 改动推回上游 git subtree push --prefix=lib/shared shared-remote feature-x ``` **优点**: - 普通 clone 拿到所有文件(无 submodule 双步骤) - 本地直接改 shared 代码 + commit(无需先 push 上游) - 简单:只是普通 git 文件 **缺点**: - `git log` 看到 shared 的所有历史(噪音) - subtree push 偶尔合并冲突复杂 - 大多数开发者不熟,命令易记错 ## C. monorepo 把所有服务和 shared 放一个 repo: ``` myorg-monorepo/ ├── services/ │ ├── api/ │ ├── worker/ │ └── frontend/ ├── packages/ │ ├── shared-models/ │ └── shared-utils/ └── tools/ ``` shared 代码就是 packages/ 下的子目录,import 直接相对路径或 workspace (pnpm / yarn workspace / npm workspace / Cargo workspace / Go workspace / etc)。 **优点**: - 改 shared + 改用它的 service 一个 commit + 一个 PR - 所有服务的 history / branch / CI 在同处 - 重构跨服务时方便(IDE refactor 全 monorepo 生效) **缺点**: - repo 越来越大(clone 慢;shallow clone 缓解) - CI 要 path-based filter(不能"改 README 跑全部测试") - 工具支持差异(Bazel / Nx / Turborepo 等 monorepo build 工具学习成本) ## D. 单独发包 shared 抽成 npm / pip 包发到内部 registry: ```bash # shared 项目 npm publish --registry https://npm.internal.example.com # 使用方 npm i @myorg/shared ``` **优点**: - 标准化语言生态 - 严格版本管理 - 不同服务可用不同 shared 版本 **缺点**: - 改 shared 要发版 → 更新 → 测试 → 多步骤 - 内部 registry 要维护 - 不适合 quick iteration ## 决策矩阵 | 场景 | 推荐 | |---|---| | 5-10 个服务 + 1 个公司 + 同一团队 | **monorepo** | | 开源项目想用别人的 OSS lib | submodule(少改) | | 想 vendor 别人代码 + 偶尔修改 | subtree | | 多公司 / 跨组织共享 lib | 单独发包 | | 多团队 + 服务有独立发版周期 | 单独发包 | | 想保持简单 + 改动频繁 | monorepo | 实际我的经验:**绝大多数场景 monorepo 是最佳选择**。submodule 是 "避免 monorepo 的 hack",长期维护成本高。 ## monorepo 工具 JavaScript / TypeScript: ```bash # package.json { "private": true, "workspaces": ["packages/*", "services/*"] } # 用 npm workspaces / pnpm workspace / yarn workspaces ``` ```bash pnpm install # 装所有 pnpm -r build # 在所有包里跑 build pnpm --filter api dev # 只对 api 包 ``` 更高级:Turborepo / Nx,加 task caching + 增量 build。 Python:用 uv workspace: ```toml # pyproject.toml [tool.uv.workspace] members = ["packages/*", "services/*"] ``` Go:go workspace: ```bash go work init go work use ./service-a ./service-b ./pkg/shared ``` Rust:Cargo workspace 在 `Cargo.toml` `[workspace]` 里。 ## CI 优化(monorepo 必备) GitHub Actions path filter: ```yaml on: pull_request: paths: - 'services/api/**' - 'packages/shared-models/**' - '.github/workflows/api.yml' jobs: test-api: ... ``` 只在 api 或它依赖的 shared 改动时跑 api 测试。 或者用 `dorny/paths-filter` action 做动态: ```yaml - uses: dorny/paths-filter@v3 id: changes with: filters: | api: 'services/api/**' web: 'services/web/**' shared: 'packages/shared-*/**' - if: steps.changes.outputs.api == 'true' || steps.changes.outputs.shared == 'true' run: pnpm --filter api test ``` 避免改一行触发全部 CI。 ## 我们的真实迁移 从 6 个独立 repo + Git submodule 到 monorepo: | | submodule before | monorepo after | |---|---|---| | 新人 clone 步骤 | 5 步 | 1 步 | | 跨 service 改动 | 6 个 PR | 1 个 PR | | build 时间(CI 全跑) | 18min | 12min(with cache: 4min) | | repo 大小 | 6 × 50MB | 280MB | | 全部签新版本 | 几小时 | 单 PR + tag | monorepo 显著简化。 ## 踩过的坑 ### submodule 类 1. **改 submodule 内代码忘 push**:在 lib/shared 改了 + 主 repo commit 更新引用 + 推主 repo。同事 clone 后 lib/shared 拿到的 commit 在 origin 不存在 → fail。**先 push submodule,再 push 主 repo**。 2. **`git checkout` 切分支 submodule 不变**:默认 git 不自动更新 submodule 到目标分支记录的版本。`git config submodule.recurse true` 让所有命令自动 recurse。 ### subtree 类 3. **subtree push 偶尔失败**:主 repo 内 commit 太散,subtree 算 patch 出错。先 squash 合并相关 commit 再 push。 ### monorepo 类 4. **某 service 重 build 全 monorepo**:必须做 path-based filter + build cache。否则 monorepo 反而比独立 repo 慢。 5. **依赖循环**:service A 用 shared B,shared B 用 service A 的某 helper(不该)→ 循环。monorepo workspace tool 应该报错;强制依赖 方向 packages/* → services/*,不反向。