知识广场

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

«计算机科学 / 软件工程» 分类下共 42 篇帖子

pre-commit 框架:跨语言项目的 git hook 统一管理

## 起因 团队仓库混 Python + Go + TypeScript + YAML,每种语言一套 linter / formatter。新人 clone 后 IDE 不一定配齐 → 推上来的 PR 各种格式不一致。 review 时纠结 "tab 还是空格" 很烦。 `pre-commit` 是 Python 写的跨语言 git hook 框架,几行 YAML 配齐 所有语言的 lint / format,commit 时自动跑。改不合规直接拒绝。 ## 解决方案 ### 1. 装 ```bash pipx install pre-commit # 或 uv tool install pre-commit ``` ### 2. `.pre-commit-config.yaml` 放仓库根: ```yaml default_install_hook_types: [pre-commit, commit-msg] default_stages: [pre-commit] repos: # 通用:尾空格 / 文件末尾换行 / 大文件 / merge conflict - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-json - id: check-added-large-files args: ['--maxkb=500'] - id: check-merge-conflict - id: detect-private-key # Python: ruff 一统 (lint + format + import sort) - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.9 hooks: - id: ruff # lint + autofix args: [--fix] - id: ruff-format # 替代 black # Go: gofmt / golangci-lint - repo: https://github.com/golangci/golangci-lint rev: v1.61.0 hooks: - id: golangci-lint # TypeScript / JavaScript / CSS / Markdown: Prettier - repo: https://github.com/pre-commit/mirrors-prettier rev: v3.1.0 hooks: - id: prettier types_or: [javascript, jsx, ts, tsx, css, scss, json, yaml, markdown] # ESLint - repo: https://github.com/pre-commit/mirrors-eslint rev: v9.10.0 hooks: - id: eslint files: \.(js|jsx|ts|tsx)$ additional_dependencies: - [email protected] - [email protected] # Shell scripts - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.10.0.1 hooks: - id: shellcheck # Dockerfile - repo: https://github.com/hadolint/hadolint rev: v2.13.0 hooks: - id: hadolint-docker # commit message: conventional commits - repo: https://github.com/compilerla/conventional-pre-commit rev: v3.4.0 hooks: - id: conventional-pre-commit stages: [commit-msg] args: [feat, fix, docs, style, refactor, perf, test, chore] ``` ### 3. install ```bash pre-commit install # .git/hooks/pre-commit 被注入 # 之后 git commit 自动触发 ``` ### 4. 试一下 ```bash echo 'x ' > test.txt # 故意尾空格 git add test.txt git commit -m 'test' # trim trailing whitespace.............................Failed # - hook id: trailing-whitespace # - exit code: 1 # - files were modified by this hook # # Fixing test.txt ``` hook 自动修了(去掉尾空格),但本次 commit 失败。再 `git add` + `git commit`: ```bash git add test.txt git commit -m 'fix: trim whitespace' # all passed! ``` ### 5. 给整个仓库初始跑一遍(修复存量问题) ```bash pre-commit run --all-files # 跑所有 hook 在所有文件上 # 第一次可能有大量改动;review 后 commit ``` ### 6. CI 也跑(防绕过) ```yaml # .github/workflows/lint.yml - uses: actions/checkout@v4 - uses: pre-commit/[email protected] ``` 开发者 commit 时绕过(`git commit --no-verify`)的情况,CI 兜底。 ### 7. 局部跳过某个 hook ```bash SKIP=ruff git commit -m 'wip: temp' ``` 不推荐常态化,应急用。 ### 8. 排除某些文件 ```yaml exclude: | (?x)^( vendor/.*| generated/.*| migrations/.*| \.min\.js$ )$ ``` 或者单 hook 排除: ```yaml - id: prettier exclude: ^public/build/ ``` ### 9. 自动更新 hook 版本 ```bash pre-commit autoupdate # 把 yaml 里所有 rev 改成各 repo 最新 release tag ``` 每月跑一次保持 hooks 最新。结果 commit 进 git。 ### 10. 自动加进 commit ```yaml - id: ruff args: [--fix] always_run: true ``` `--fix` 让 ruff 自动改。改完文件后 hook fail(让你重新 add)。 更激进的"hook 失败也不阻拦 commit": ```bash pre-commit run --hook-stage manual ``` 把 `stages: [manual]` 的 hook 设为"手动跑",commit 时不阻拦。 ## 与 husky / lefthook 对比 | | pre-commit | husky | lefthook | |---|---|---|---| | 语言无关 | ✅ | 半 (Node 项目优先) | ✅ | | 装 / 配 | Python | npm | 二进制 | | hook 仓库化 | ✅ pre-commit-hooks 大量 | 自己写 | 自己写 | | 性能 | 中(Python 启动) | 快 | 极快 (Go) | | 易用 | 高 | 高 | 中 | 混语言仓库 / Python 项目 → pre-commit;纯 Node 项目 → husky 也行; 极致性能 → lefthook。 ## monorepo 多服务 ```yaml # 给每个子目录的 hook 限制路径 - id: ruff files: ^backend/ - id: eslint files: ^frontend/ ``` 避免在 backend 改 Python 时跑 frontend eslint。 ## 效果 我们仓库接入 pre-commit 后: - PR 里 "形式修改" commit 消失(已经在本地修了) - review 只关注业务逻辑 - 新人 clone 后 `pre-commit install` 一次,之后所有规范自动 enforce - CI lint 时间从 5 分钟 → 1 分钟(pre-commit 已经修好的不重检) ## 踩过的坑 1. **pre-commit 第一次跑超慢**:每个 hook 创建 isolated env 装依赖。 `.pre-commit-cache` 缓存几个 GB 之后才稳定。CI 里 cache 该目录。 2. **hook 改了文件 → commit 失败**:合理设计,但新人会困惑。 告诉团队"hook fail 看下面输出 + 重新 git add + commit"流程。 3. **`SKIP=` 滥用**:成员养成习惯跳所有 hook。在 PR 模板里加"是否跑了 pre-commit"checkbox。CI 一定要跑兜底。 4. **autoupdate 引入 breaking change**:新版 ruff 突然报 50 个新警告。 autoupdate 后单独 PR 修问题再合主。 5. **hook 内拉 docker image**:`hadolint-docker` hook 第一次拉 image 慢。可以换成本地 `hadolint`(先 `brew install hadolint`)+ `repo: local`。

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:` 描述。

mise:替代 nvm + pyenv + rbenv + ... 的多语言版本管理器

## 起因 机器上同时维护 4 个项目: - 一个 Node 16 老仓库 - 一个 Node 20 新项目 - 一个 Python 3.9 后端 - 一个 Python 3.12 数据 pipeline 之前装了 nvm + pyenv + rbenv + goenv。每个工具一套 shell hook, shell 启动时间 1.5 秒(全是 PATH 操作)。切项目时 `nvm use` `pyenv shell` 排队按。 `mise`(前身 rtx)一个工具管所有语言版本,shell 启动只加 50ms。 ## 安装 ```bash # 一行装 curl https://mise.run | sh # 或包管理器 brew install mise sudo apt install mise # 装完接到 shell echo 'eval "$(mise activate bash)"' >> ~/.bashrc echo 'eval "$(mise activate zsh)"' >> ~/.zshrc echo 'mise activate fish | source' >> ~/.config/fish/config.fish mise --version ``` ## 装语言 ```bash # 全局装版本 mise use --global node@20 mise use --global [email protected] mise use --global [email protected] mise use --global rust@stable # 在某项目目录里指定(写入 .mise.toml 或 .tool-versions) cd ~/projects/legacy-app mise use node@16 # 这个目录用 Node 16 cd ~/projects/data-pipeline mise use [email protected] # 自动切换:cd 进目录 mise 自动 use 对应版本 ``` ## .mise.toml 项目配置 ```toml # .mise.toml [tools] node = "20" python = "3.12" go = "1.22" rust = "stable" "npm:pnpm" = "latest" # 通过 npm 装 pnpm "pipx:poetry" = "latest" # 通过 pipx 装 poetry "go:github.com/jesseduffield/lazygit" = "latest" [env] DATABASE_URL = "postgresql://localhost/myapp" PYTHONDONTWRITEBYTECODE = "1" [tasks.test] description = "Run tests" run = ["pytest", "npm test"] [tasks.dev] description = "Start dev server" run = "npm run dev" ``` 之后: ```bash mise install # 装 .mise.toml 声明的所有工具 mise run test # 跑 [tasks.test] mise run dev mise tasks # 列所有 tasks ``` ## 列出 / 切换 ```bash mise ls # 看当前激活的工具版本 mise ls --installed # 看本机装了哪些版本 mise outdated # 看哪些工具有新版本 mise current # 当前目录最终生效的版本(含继承) mise where node # 当前 node 二进制路径 ``` ## env 管理 `mise.toml` 的 `[env]` 段在 mise activate 后 cd 进目录自动注入: ```toml [env] AWS_PROFILE = "dev" DATABASE_URL = "postgresql://localhost/myapp" _.path = ["./bin", "./node_modules/.bin"] # 加 PATH _.file = ".env" # 也读 .env 文件 ``` 替代了 direnv 大部分用法。 ## 与 .tool-versions(asdf 兼容) asdf 用户已有 `.tool-versions`: ``` nodejs 20.10.0 python 3.12.1 ruby 3.2.0 ``` mise 直接读,零迁移。新项目也建议用更强大的 `.mise.toml` 格式。 ## 团队协作 把 `.mise.toml` 进 git: ```bash git add .mise.toml git commit -m 'chore: pin tool versions via mise' ``` 新人 clone 后: ```bash cd repo mise install # 装齐所有工具版本 ``` CI 里: ```yaml - uses: jdx/mise-action@v2 with: cache: true - run: mise run test ``` ## 性能对比 | | 启动延迟 | 多语言 | |---|---|---| | nvm | 500-1500ms | ❌ | | pyenv | 200-400ms | ❌ | | asdf | 200-500ms | ✅ | | **mise** | 30-50ms | ✅ | Rust 写的 + 智能 PATH shim 让启动几乎瞬时。 ## 替代了什么 机器上以前装的: - nvm → 删 - pyenv → 删 - rbenv → 删 - goenv → 删 - direnv(大部分用法)→ 删(保留给复杂 shell 逻辑) `.zshrc` 从 80 行清到 30 行,shell 启动 1.5s → 0.2s。 ## mise tasks vs Makefile `mise tasks` 比 Makefile 优势: - 不需要 tab 缩进 - 跨平台一致(make 在 Windows 不友好) - 自动激活该项目的工具版本 - TOML 比 Makefile 易读 ```toml [tasks.lint] run = ["ruff check .", "mypy src/"] [tasks.fmt] run = ["ruff format .", "ruff check --fix ."] [tasks.ci] depends = ["lint", "test"] run = "echo all green" [tasks.test] run = "pytest --cov" ``` ```bash mise run ci # 自动跑 lint + test ``` ## 效果 - 4 个项目切换无感(cd 进去自动切版本) - shell 启动 < 0.2s - 一个工具管所有语言:心智模型干净 - .mise.toml 进 git 让新同事 onboard "git clone + mise install" 完事 - task runner 顺带替代了一半 Makefile / package.json scripts ## 踩过的坑 1. **从 nvm 迁过来 PATH 顺序乱**:nvm 残留 PATH 里。彻底清 nvm: ```bash rm -rf ~/.nvm # ~/.bashrc 里删掉所有 nvm 相关行 ``` 2. **mise 后台编译 Python / Ruby 慢**:源码编译需要 build-essential + libssl-dev 等系统库。Debian/Ubuntu: ```bash sudo apt install -y build-essential libssl-dev libffi-dev \ libsqlite3-dev libbz2-dev libreadline-dev zlib1g-dev \ libncurses-dev liblzma-dev tk-dev ``` 3. **CI 没缓存导致每次都重装**:mise-action 加 `cache: true`。 4. **某些工具用 GitHub release 装**:网络问题在国内有时拉不下来。 设 `MISE_GITHUB_TOKEN` 或者镜像源。 5. **VSCode Python 解释器找不到**:mise 装的 Python 在 `~/.local/share/mise/installs/python/3.12.0/bin/python`。 `.vscode/settings.json` 写 `"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"` 让它用项目 venv。

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`。

ripgrep(rg)替代 grep:尊重 .gitignore + 自动忽略二进制 + 快 10 倍

`rg` 是 ripgrep 的命令行工具。比 `grep -r` 快一个数量级(Rust + SIMD), 默认行为更符合代码库搜索的预期:跳过 `.git`、尊重 `.gitignore`、 跳过二进制文件、自动多线程。 ## 安装 ```bash sudo apt install -y ripgrep # Debian 11+ / Ubuntu 20.04+ brew install ripgrep # macOS choco install ripgrep # Windows rg --version ``` ## 最基础 ```bash rg pattern # 在当前目录递归搜 pattern rg pattern src/ # 在 src/ 里搜 rg -i pattern # 大小写不敏感 rg -w word # 整词匹配 rg -v 'never' # 反向(不含的行) rg -F 'a.b.c' # 字面字符串(不当 regex) ``` 输出格式: ``` src/utils/string.py 12: def normalize(s: str) -> str: 14: return s.strip().lower() ``` 文件名 + 行号 + 高亮。 ## 文件类型筛选 ```bash rg pattern -t py # 只在 Python 文件搜 rg pattern -T md # 排除 Markdown rg pattern -g '*.tsx' # glob 匹配 rg pattern -g '!**/node_modules/**' # glob 排除 rg --type-list # 列所有已知文件类型 ``` `-t`、`-T` 让命令行干净得多——`rg -t py error` vs `grep -r --include='*.py' error .`。 ## 显示前后行(context) ```bash rg pattern -A 3 # after 3 行 rg pattern -B 3 # before 3 行 rg pattern -C 3 # context 前后各 3 行 ``` 跟 grep 完全一样的参数。 ## 只列文件名 ```bash rg -l pattern # 只列匹配的文件名 rg --files-without-match pattern # 反过来,列不匹配的文件 ``` `-l` 配合 fzf / xargs 极常用: ```bash # 编辑所有匹配的文件 vim $(rg -l 'TODO') ``` ## 替换(dry-run + 实际改) ```bash # dry-run:只显示替换效果,不写入 rg 'old_name' --replace 'new_name' # 真的批量替换:rg 找出 + sed 改 rg -l 'old_name' | xargs sed -i 's/old_name/new_name/g' # macOS 上 sed -i '' '...' ``` 或者用 `sd`(rust 写的 sed 替代): ```bash rg -l 'old_name' | xargs sd 'old_name' 'new_name' ``` ## 输出 JSON(pipeline 友好) ```bash rg --json pattern src/ | head # {"type":"begin","data":{"path":{"text":"src/foo.py"}}} # {"type":"match","data":{"path":...,"lines":...,"line_number":12,...}} # {"type":"end",...} ``` 让别的工具处理结构化输出。 ## 通用预设:~/.ripgreprc ``` # ~/.ripgreprc --smart-case --max-columns=200 --colors=line:fg:yellow --colors=path:fg:blue --type-add=web:*.{html,css,js,jsx,ts,tsx,vue} --type-add=md:*.{md,markdown,mdx} ``` ```bash echo 'export RIPGREP_CONFIG_PATH=~/.ripgreprc' >> ~/.bashrc ``` 之后 `rg pattern` 自动用这套默认。 ## 与 grep 对比 | 任务 | grep | rg | |---|---|---| | 在仓库找 `TODO` | `grep -r --include='*.py' TODO .` | `rg -t py TODO` | | 跳过 .git / node_modules | 要手动 `--exclude-dir` | 自动 | | 二进制文件 | 默认搜(输出乱码) | 自动跳过 | | 速度(100k LOC) | 几秒 | < 0.5s | | 默认大小写 | 区分 | smart-case (有大写=区分) | `grep` 仍然是 POSIX 工具,远程小机器没 rg 时用。日常本地 / CI 用 rg。 ## 与 fzf 联用 ```bash # 模糊搜代码内容 rg . --line-number --no-heading --color=always \ | fzf --ansi --delimiter=':' \ --preview 'bat --color=always {1} --highlight-line {2}' ``` 写成函数(前面 fzf 那篇有提到)。 ## VSCode 内部用了它 VSCode 的全局搜索(Ctrl-Shift-F)底层就是 ripgrep。任何用 rg 直接命令行的 都和 IDE 搜索一致体验。 ## 性能:为什么这么快 - 用 Rust 写 + Aho-Corasick / RE2 / SIMD - 多线程:每个文件并行搜 - mmap:大文件 zero-copy - gitignore 解析:跳过整个子树而不是访问每个文件 - 二进制检测:发现 NUL 字节立即跳过整个文件 ## 边界 / 别用 rg 的场合 - 单文件的复杂正则:`grep -P` 的 PCRE 比 rg 默认的 Rust regex 更全 (lookbehind 等)。rg 加 `-P` 也能用 PCRE2,但需要 build 时打开特性。 - 流式(stdin)+ 极简:`grep` 在 docker shell / 嵌入式环境永远可用。 - 颜色输出走 pipeline:rg 默认看到 stdout 不是 tty 就关闭颜色, 和别的工具兼容。 ## 踩过的坑 - 想搜 `.git` 里的东西时 rg 默认跳过 → 加 `-uu`("unrestricted" 二次: 搜隐藏 + 不尊重 .gitignore + 二进制)。 - `.gitignore` 有 `*.log` 时 rg 不会搜日志文件 —— 通常正确,但调试时 忘了为什么没结果。`rg -u pattern` 一次性绕过 ignore 规则。 - 重定向到文件后颜色没了:`rg --color=always pattern | less -R` 强制保留颜色。 - 中文 / 非 ASCII 搜索:rg 默认 UTF-8 兼容;老 BOM / GBK 文件需要 `--encoding gbk`。

httpie / xh / hurl:替代 curl 做 API 测试的现代选择

## 起因 每天调几十次 curl 测 API,命令越写越长: ```bash curl -X POST https://api.example.com/v1/users \ -H 'Authorization: Bearer xxx' \ -H 'Content-Type: application/json' \ -d '{"email":"[email protected]","name":"Alice","age":30}' \ -i -v -s \ | jq . ``` curl 强大但 verbose。下面几个现代工具针对"开发期 API 调试"做了大幅 简化。 ## httpie:人友好的 curl 替代 ```bash brew install httpie # 或 apt install httpie / pipx install httpie http --version ``` ### 用法 ```bash # GET http https://api.example.com/users/1 # 自动 pretty-print JSON + 语法高亮 # POST + JSON body http POST https://api.example.com/users \ [email protected] name=Alice age:=30 # - key=value 是字符串 # - key:=value 是 number/bool/null 等 (JSON 类型) # - 自动加 Content-Type: application/json # Authorization http https://api.example.com/me Authorization:'Bearer xxx' # 或: http -A bearer -a xxx https://api.example.com/me # 上传文件 http POST https://api.example.com/upload file@./photo.jpg # 看 request / response 头(默认显示 response 头) http -v POST ... # -v 把 request 也打印 http -h POST ... # 只 print headers http -p Hb POST ... # 控制哪些部分打印 (H=header, b=body) # session(保存 cookie + auth) http --session=alice POST https://api.example.com/login [email protected] http --session=alice https://api.example.com/me # 复用 cookies ``` 输出: ``` HTTP/1.1 200 OK Content-Type: application/json Date: Mon, 24 May 2026 10:00:00 GMT Server: nginx { "id": 42, "name": "Alice", "email": "[email protected]" } ``` JSON 自动格式化 + 高亮,headers 区分颜色,整体一眼看清。 ## xh:Rust 重写的 httpie,启动更快 ```bash brew install xh # 或 cargo install xh xh --version ``` API 几乎完全跟 httpie 兼容: ```bash xh POST https://api.example.com/users [email protected] name=Alice ``` httpie 启动 ~150ms(Python),xh ~10ms(Rust)。频繁使用差异明显。 我个人现在用 xh > httpie > curl 这个偏好。 ## hurl:把 API 测试写成可重放脚本 ```bash brew install hurl ``` `.hurl` 文件: ``` # login.hurl POST https://api.example.com/login { "email": "[email protected]", "password": "secret" } HTTP 200 [Asserts] jsonpath "$.token" exists jsonpath "$.user.email" == "[email protected]" [Captures] token: jsonpath "$.token" GET https://api.example.com/me Authorization: Bearer {{token}} HTTP 200 [Asserts] jsonpath "$.email" == "[email protected]" POST https://api.example.com/posts Authorization: Bearer {{token}} { "title": "Hello", "body": "..." } HTTP 201 [Asserts] jsonpath "$.title" == "Hello" [Captures] post_id: jsonpath "$.id" GET https://api.example.com/posts/{{post_id}} HTTP 200 ``` 跑: ```bash hurl login.hurl # 全部 assertion 通过 = exit 0 # 否则 exit 非 0 # 输出更详细 hurl --verbose --variables-file vars.env login.hurl # 测试 mode hurl --test login.hurl ``` 适用: - E2E API 测试 + CI - API 回归验证(每次部署后自动跑) - 复制粘贴前端测试用的 request 序列 完整功能:query params、文件上传、cookies、多文件 chain、报告输出(JSON/JUnit/HTML)。 ## curl 仍然适合什么 curl 不会消失: - 服务器 minimal 环境(绝对默认安装) - 调试 TLS / HTTP 协议级(`-v --trace`) - script / Dockerfile / CI 里(无依赖、跨平台) - 二进制下载 / 长文件传输 httpie / xh 是"开发期人调用",curl 是"机器调用"。 ## 与 Postman / Insomnia 对比 GUI 工具优势: - 历史记录 + 文档化 - 团队共享 collection - 自动 OAuth 流程 CLI 工具优势: - 嵌入 shell 脚本 - 版本控制(.hurl 进 git) - 远程服务器跑(无 GUI 环境) 我个人:探索 / 团队共享用 [Bruno](https://www.usebruno.com/)(GUI 但 本地文件存储 + git 友好);shell 直接调用 xh / hurl。 ## bonus: jq / jaq 处理 JSON 响应 ```bash xh https://api.example.com/users | jq '.[] | select(.age > 30) | .name' # jaq(jq 的 Rust 重写,10x 快) brew install jaq xh https://api.example.com/users | jaq -r '.[] | .email' ``` `-r` raw 输出(无引号),适合 pipe 进 xargs。 ## 实战 workflow 测一个新 API: ```bash # 1. 探索 - GET xh https://api.example.com/users/1 # 2. 试 POST xh POST https://api.example.com/users name=Bob [email protected] # 3. 满意后写成 hurl 脚本 cat > tests/create-user.hurl <<'EOF' POST https://api.example.com/users { "name": "{{name}}", "email": "{{email}}" } HTTP 201 [Asserts] jsonpath "$.id" exists EOF # 4. CI 里跑 hurl --test --variables-file ci-vars.env tests/*.hurl ``` xh 探索 → hurl 固化。一条 pipeline。 ## 效果 迁移后: - 日常 API 调试输入字符 -70%(不用 `-X` `-H` `-d`) - API 回归测试用 hurl 脚本进 git,新员工看脚本就懂用法 - ci 端 hurl 测试 10 个 endpoint 用 5 秒 - 团队同 hurl 文件分享,跨 OS / 跨编辑器一致 ## 踩过的坑 1. **shell 解析 `:` 和 `=`**: ```bash http POST url 'description=hello, world' # 逗号正常 http POST url 'tags:=["a","b"]' # JSON 数组要 quote ``` 特殊字符记得 quote。 2. **httpie 默认 redirect 不跟**:`-F` / `--follow` 才跟 redirect。 xh 默认跟。 3. **session 文件路径**:`~/.config/httpie/sessions/`。换机器要带, 或 `--session-read-only` 临时用。 4. **hurl asserts 严格类型**:`jsonpath "$.count" == 5` 和 `== "5"` 不同(数字 vs 字符串)。看 API 返回类型对照写。 5. **不支持 WebSocket / gRPC**:纯 HTTP。WebSocket 用 wscat / websocat; gRPC 用 grpcurl。

atuin:跨机器同步 + 加密的 shell 历史

## 起因 shell 历史的痛点: - 几台机器(笔记本 / 台式 / 远程 server)历史不互通 - Ctrl-R 反搜慢 + 一次显一条 - 历史没 metadata(哪个目录跑的?exit code?耗时?) - 关闭终端 history 偶尔丢 `atuin` 是 Rust 写的 shell history 替代: - 历史存 SQLite,带元数据(cwd、exit、duration、host、session) - E2E 加密同步到 server(自己 host 或官方) - Ctrl-R 替换成全屏 fuzzy search UI ## 装 ```bash brew install atuin curl --proto '=https' --tlsv1.2 -LsSf https://setup.atuin.sh | sh # shell 集成 atuin init zsh >> ~/.zshrc atuin init bash >> ~/.bashrc atuin init fish | source ``` import 现有历史: ```bash atuin import auto # 检测 shell 自动 import ``` ## 第一次用 Ctrl-R 启动 atuin 全屏 search: ``` atuin search ~/proj/myapp ──────────────────────────────────────── > docker 10s /home/u/proj docker compose up -d ✓ 2m /home/u/other-proj docker logs api -f ✓ 1h /home/u/proj docker compose ps ✓ yesterday /home/u/proj docker compose build ✓ ──────────────────────────────────────────────────────────────────── filter <ctrl-r> | invert <ctrl-a> | exit <esc> ``` - 实时 fuzzy 过滤 - 显示运行时间 / cwd / 是否成功(✓/✗) - enter 执行 / tab 编辑 `Ctrl-R` 一秒查 10 年前在哪台机器跑过啥命令。 ## sync 同步 注册账号: ```bash atuin register -u me -e [email protected] atuin login -u me # import 上来的 + 之后的命令都加密同步到 cloud ``` E2E 加密:服务端只存 ciphertext,端上解密。 默认用 atuin.sh 官方服务(免费);介意 → 自部署: ```bash docker run -d -p 8888:8888 ghcr.io/atuinsh/atuin server start ``` `~/.config/atuin/config.toml`: ```toml sync_address = "https://your-server.com" ``` ## 跨机器登录 新机器: ```bash brew install atuin atuin login -u me -p <password> -k <encryption-key> atuin sync ``` `-k` 是首次注册时 atuin 显示的加密 key,**自己存好**(atuin server 无法恢复 key)。 `atuin sync` 把云端历史拉下来 + 解密 → 立刻有所有机器的命令。 ## 配置过滤 不想同步某些命令: ```toml # ~/.config/atuin/config.toml # 不记录 secret 命令 history_filter = [ "^secret-cmd", "^aws.*--secret", ] # 不在公共 wifi 同步 sync_address = "..." auto_sync = true sync_frequency = "5m" ``` ## stats ```bash $ atuin stats Top commands: 1. git status (1234) 2. ls (987) 3. cd .. (876) ... Total commands: 45678 First command: 2022-03-14 ``` 或者: ```bash $ atuin stats day $ atuin stats week $ atuin stats --since '1 month ago' ``` 看一个月你跑啥最多。会启发优化 alias / 工作流。 ## 各机器 history 分别看 ```bash atuin search --cwd . # 当前目录历史 atuin search --hostname laptop # 在 laptop 跑的 atuin search --exit 0 # 只成功的 atuin search 'docker' --before '1 week ago' ``` ## 内置 vs atuin | | shell 内置 history | atuin | |---|---|---| | 存储 | ~/.zsh_history (text) | SQLite | | 同步 | 无 | 跨机器加密 | | 元数据 | 无 | cwd / exit / duration | | 搜索 | 反搜(单行) | TUI fuzzy | | 容量 | 几千-几万行 | 无限 | | 性能 | 文件 grep | 索引 query | 不会用 atuin 后就回不去了。 ## 性能 `atuin search` 上百万 history entry sub-50ms 响应。SQLite 索引 + Rust 解析快。 启动开销:shell 集成 ~5-10ms(vs no atuin ~0ms)。基本无感。 ## 替代品 - **mcfly**(rust):类似 atuin,没 sync - **hstr**:传统 TUI,无元数据 - **fzf + fc**:fzf 模糊搜历史(无元数据) atuin 是目前最完整方案。 ## 退出策略 万一不喜欢: ```bash # 取消 shell 集成(从 .zshrc 删 atuin init) # 导出回 plain history atuin export csv > history.csv # 删 SQLite rm -rf ~/.local/share/atuin ``` history 不会"绑定"住你。 ## 隐私 / 安全 - 数据库本地:`~/.local/share/atuin/history.db`,未加密 → 笔记本被偷 对方能读历史 - 云端:E2E 加密,server 看不到内容 - 命令含 secret:`history_filter` 排除 担心本地泄密 → FileVault / LUKS 全盘加密 + atuin filter sensitive 命令。 ## 一个 case:debug 老问题 3 个月后客户报"上次配的某个 nginx 设置不工作了"。 我不记得当时改了啥。 ```bash $ atuin search --since '4 months ago' --before '2 months ago' nginx ``` 10 个命令列出来:当时 edit 啥配置、reload、看 log。 2 分钟还原情境。比"翻 commit / 看 wiki"快多了。 shell history 是被低估的 personal knowledge base。 ## 踩过的坑 1. **首次同步慢**:百万 entry 上传几分钟。耐心等。 2. **重复 history**:bash + zsh 都用 atuin → 同命令记两次。每 shell 只用一个。 3. **导入 fish 历史 format 不对**:fish 用 yaml 历史,老版本 atuin import 解析坑。新版本修了。 4. **`Up` 键被吃**:atuin 默认绑 `Up` 显示历史 → 跟 vi mode 冲突。 `disable_up_arrow = true` 关。 5. **加密 key 丢了**:服务端有数据但解密不了。**`atuin register` 时 立刻把 key 存密码管理器**。

hyperfine:CLI 命令 benchmark 工具

## 起因 想知道哪个命令更快: - `find . -name '*.py'` vs `fd -e py` - `cat file | grep foo` vs `rg foo file` - 两个不同 build 工具 老办法 `time cmd`:跑一次结果有噪声,不科学。 `hyperfine`(Rust)专门 benchmark:自动多次跑 + warmup + 统计 + 对比 + 优雅输出。 ## 装 ```bash brew install hyperfine cargo install hyperfine ``` ## 基本 ```bash $ hyperfine 'find . -name "*.py"' Benchmark 1: find . -name "*.py" Time (mean ± σ): 345.2 ms ± 12.3 ms [User: 90.1 ms, System: 245.6 ms] Range (min … max): 330.1 ms … 365.8 ms 10 runs ``` 自动跑 10 次 + mean / σ / range。 ## 对比两命令 ```bash $ hyperfine 'find . -name "*.py"' 'fd -e py' Benchmark 1: find . -name "*.py" Time (mean ± σ): 345.2 ms ± 12.3 ms Benchmark 2: fd -e py Time (mean ± σ): 62.1 ms ± 3.4 ms Summary 'fd -e py' ran 5.56 ± 0.36 times faster than 'find . -name "*.py"' ``` 直接看 fd 比 find 5.5x 快。 ## warmup 第一次跑可能 cold cache: ```bash hyperfine --warmup 3 'cmd' ``` 跑 3 次 warmup(不计时)+ 后面 10 次计时。 避免 disk cache miss 干扰。 ## prepare / cleanup ```bash hyperfine --prepare 'sync; echo 3 > /proc/sys/vm/drop_caches' 'cmd' ``` 每次 run 前 prepare(drop cache 测 cold path)。 或者每次后 cleanup(删 output file 之类)。 ```bash hyperfine --cleanup 'rm output.txt' 'process input > output.txt' ``` ## 参数 sweep ```bash hyperfine --parameter-list n 100,1000,10000 'sleep 0.{n}' ``` 跑 sleep 0.100 / 0.1000 / 0.10000 各 10 次 → 输出 sweep 表格。 例:测不同 thread 数: ```bash hyperfine --parameter-scan threads 1 8 'make -j{threads}' ``` 1-8 thread 各跑 make → 看 sweet spot。 ## export 数据 ```bash hyperfine 'cmd1' 'cmd2' --export-markdown result.md hyperfine 'cmd1' 'cmd2' --export-json result.json hyperfine 'cmd1' 'cmd2' --export-csv result.csv ``` Markdown 直接贴 PR 比较 before/after。 ## 真实 case 1:CI 测试加速 CI 跑 pytest 改 plugin: ```bash hyperfine --warmup 1 \ 'pytest -p no:cacheprovider' \ 'pytest' \ 'pytest -n 4' \ 'pytest -n auto' ``` | | mean | |---|---| | pytest (no cache) | 45s | | pytest (cache) | 38s | | pytest -n 4 | 14s | | pytest -n auto (8 cores) | 9s | 加 -n auto = 5x 加速 → CI yaml 一行改动收益巨大。 ## 真实 case 2:build 工具对比 ```bash hyperfine --warmup 2 --prepare 'rm -rf node_modules dist' \ 'npm install && npm run build' \ 'pnpm install && pnpm build' \ 'bun install && bun run build' ``` 得出"对此项目 pnpm 比 npm 快 2x"。 凭印象不如实测。 ## ignore failure ```bash hyperfine --ignore-failure 'might_fail_cmd' ``` 某些工具偶尔 fail 但你想 measure 成功的部分。 ## 数据可视化 ```bash hyperfine 'cmd1' 'cmd2' --export-json result.json python -m hyperfine.plot.histogram result.json ``` 直方图看 distribution(不只是 mean)。 发现"通常 1s 偶尔 10s" 的长尾。 ## warmup 不够准 ```bash hyperfine --runs 100 'cmd' ``` 跑 100 次 → CLT 收敛 → 更准 mean / σ。 缺点:慢命令做不到。 ## 与 perf / criterion 对比 - `perf stat cmd`:Linux 性能 counter(cache miss / branch misses 等),更深 - `criterion`(Rust lib):微基准,函数级别 - `hyperfine`:命令级别比较 hyperfine 是"对比工具 / shell 命令"的瑞士军刀。 深入 profile 用 perf。 ## 与 ab / wrk 对比 `ab` / `wrk` 测 HTTP server(并发 + RPS)。 hyperfine 测**单 invocation 时间**。 ```bash # 测 server 响应(应该用 wrk) wrk -t 4 -c 100 -d 30s http://localhost:8000/ # 测命令耗时 hyperfine 'curl http://localhost:8000/' ``` 不同用途。 ## 我的常用 alias ```bash alias bench='hyperfine --warmup 2' alias bench-cold='hyperfine --warmup 0 --prepare "sync; echo 3 | sudo tee /proc/sys/vm/drop_caches"' ``` 测优化前后效果: ```bash git stash # 老代码 bench 'cmd' git stash pop # 新代码 bench 'cmd' ``` 或者一次同时跑: ```bash git stash hyperfine 'cmd' --export-json before.json git stash pop hyperfine 'cmd' --export-json after.json hyperfine-compare before.json after.json ``` ## 踩过的坑 1. **stdout 输出大**:命令 print 几 MB → buffer 影响 measurement。 `> /dev/null` redirect。 2. **shell startup overhead**:测 sub-ms 命令 → shell fork 本身就 几 ms。极短命令 hyperfine 不准。 3. **multi-core 干扰**:测时其它进程跑 → 数字飘。`taskset 0x1 hyperfine ...` 绑核 isolation。 4. **测试 build 重复 work**:第二次 build incremental → 比第一次快。 `--prepare 'rm -rf build/'` 公平。 5. **system noise**:disk encryption / antivirus / Time Machine 等 背景影响。多次跑 + 看 σ。

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。

lazygit:终端里的 git GUI,告别一长串 git 命令

## 起因 git 命令多 + 难记。常见操作经常要 4-5 个步骤: - 看 diff → stage 部分 → unstage 错的 → commit → push - 找某个 commit 看做了啥 → cherry-pick → push - rebase 时改某个 commit message - 一个文件 history 看每行 blame 老办法:`git status` + `git diff` + `git add -p`(交互 hunk)+ `git commit` ... 一连串。 GUI 方案(GitKraken / Tower / Sourcetree)功能强但要离开终端 + 收费 + 启动慢。 `lazygit` 是终端 TUI,5 秒启动 + 键盘操作 + 覆盖 90% git 工作流。 ## 装 ```bash brew install lazygit # 或 apt install lazygit # ubuntu 22.04+ ppa ``` ## 启动 仓库目录里: ```bash lazygit ``` 或者 alias `lg='lazygit'`。 UI 5 个 panel: ``` ┌─Status─────┬─Files──────────────────────────────────┐ │ master │ M app/views.py │ │ │ ?? new_file.md │ ├─Branches───┤ │ │ master ├─Diff/Output──────────────────────────┤ │ feature │ + new line │ │ │ - old line │ ├─Commits────┤ │ │ abc Update │ │ │ def Fix │ │ └────────────┴──────────────────────────────────────┘ ``` `Tab` 切 panel,`?` 看快捷键。 ## 常用操作 ### stage / commit / push ``` 1. j/k 在 Files panel 选文件 2. space 切换 stage / unstage 3. enter 进 hunk view,space stage 单个 hunk 4. c 写 commit message 5. P push ``` vs `git add -p` 交互的痛苦:lazygit 可视化 + 按键即操作。 ### 看 diff 选文件 → 右边 panel 自动显示 diff。 高亮 / 上下滚(PgUp/PgDn) / 同步两窗口(vimdiff like 用 i 触发)。 ### 切换分支 Branches panel: ``` 1. Tab 到 Branches 2. j/k 选分支 3. space checkout 4. n 新建分支 5. d 删除 ``` ### rebase / cherry-pick Commits panel: ``` 1. Tab 到 Commits 2. r 选中 commit 改 message 3. s squash 进上一个 commit 4. f fixup(squash + 用上一个 message) 5. e edit commit(修改文件后 continue) 6. d drop commit ``` interactive rebase 不用记 `git rebase -i HEAD~5` + vim 编辑 todo list。 lazygit 里上下选 + 按键。 ### stash 操作 ``` S # stash all g # 弹 stash 列表 space # pop ``` ### log + blame ``` y # show log graph b # blame current file ``` ### remote 操作 ``` P # push(可选 force / 不同 remote) p # pull f # fetch ``` ## 配置 `~/.config/lazygit/config.yml`: ```yaml gui: theme: lightTheme: false showCommandLog: false nerdFontsVersion: "3" # 用 NerdFont 图标 git: autoFetch: false # 默认每分钟 fetch,关掉省网络 paging: colorArg: always pager: delta --paging=never # 用 delta 渲染 diff keybinding: files: commitChanges: "c" customCommands: - key: "C" description: "Commit conventional" command: "git commit -m '{{.Form.Type}}({{.Form.Scope}}): {{.Form.Msg}}'" context: "files" prompts: - type: "menu" title: "Type" options: - { name: "feat", value: "feat" } - { name: "fix", value: "fix" } - { name: "chore", value: "chore" } - type: "input" title: "Scope" key: "Scope" - type: "input" title: "Message" key: "Msg" ``` customCommands 强大:自定义快捷工作流。`Shift-C` 启动 conventional commit prompt。 ## 与 git 命令对比 | 操作 | git 命令 | lazygit | |---|---|---| | stage 部分 | `git add -p` + y/n | space space space | | commit | `git commit -m "..."` | c → 输入 | | 切分支 | `git checkout xxx` | Tab → 选 → space | | rebase 改 message | `git rebase -i HEAD~3` + edit | r | | stash pop | `git stash pop` | g → space | | 看历史 | `git log --oneline` | 主屏右下自动 | 熟练后 lazygit **快 2-3x**。新人上手时间从 git CLI 几周 → lazygit 几小时。 ## 与 GitUI / tig / gitk 对比 - **GitUI**:Rust 写,比 lazygit 启动还快,但功能略少(无 customCommands) - **tig**:老牌,只读 history 浏览强,操作交互弱 - **gitk**:Tcl/Tk GUI,老土但 history graph 经典 我用 lazygit 做日常 + tig 看复杂 history graph。 ## 与 IDE 内置 git 工具对比 VS Code / JetBrains 自带 git GUI 也好用。 lazygit 优势: - 在终端里(vim / tmux 用户友好) - 跨编辑器一致体验 - 远程 ssh 服务器也能用(GUI 没法用) 我同时用:IDE 看 diff / cherry-pick UI;lazygit 做 rebase / stage。 ## tmux popup 启动 `~/.tmux.conf`: ``` bind g display-popup -E -w 90% -h 90% lazygit ``` 任意窗口 `prefix+g` 弹 lazygit popup → 操作完关掉。完美。 ## 踩过的坑 1. **大仓库慢**:几十万 commit 的 repo(如 chromium)lazygit 启动几秒 load。`logCmd` config 可以限制。 2. **submodule 不直观**:lazygit 对 submodule 支持有限。 submodule 切换要 enter 进 sub view。 3. **conflict 解决**:merge conflict 时 lazygit 提示要手动编辑文件再回来。 有简单的 file marker view 但复杂 conflict 还是开编辑器。 4. **commit 模板没用上**:git `commit.template` 配的模板 lazygit commit 命令不自动用。要在 lazygit config 里另外配 `commitPrefix`。 5. **远程 / fetch 不显**:拉新分支后 lazygit 没自动刷新。R refresh 一下。

fzf 在终端里模糊搜索任意东西(文件、历史、分支、进程)

`fzf` 是命令行模糊搜索工具,几乎所有 "我要从一堆东西里挑一个" 的场景 都能用。一旦养成习惯,效率提升明显。 ## 安装 ```bash # Debian/Ubuntu (22.04+) sudo apt install -y fzf # 或最新版(推荐) git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` `install` 脚本会问你要不要装 shell key binding + completion,都选 y。 ## 基本 shell 集成 装完后,bash / zsh 里有这几个快捷键: - **Ctrl-T**:把"文件路径"插到光标处(fuzzy 选择当前目录下的文件) - **Ctrl-R**:搜索 shell 命令历史 - **Alt-C**:fuzzy cd 到子目录 - **`** `Tab`**:通用 completion(命令、文件名、kill PID 等) 最常用的是 **Ctrl-R** —— 一旦用上就忘不掉。 ## 常用模式 ### 选文件打开 ```bash vim "$(fzf)" ``` ### 选分支 checkout ```bash git checkout "$(git branch --all | fzf | sed 's/^..//; s/remotes\/origin\///')" ``` ### kill 进程 ```bash ps -ef | fzf -m | awk '{print $2}' | xargs kill # -m 多选 ``` ### 选 docker 容器进入 ```bash docker exec -it "$(docker ps | fzf | awk '{print $1}')" bash ``` ## 写成函数 / alias ```bash # 模糊 cd fcd() { cd "$(fd -t d --hidden --exclude .git . "${1:-.}" | fzf)" } # fzf-grep:模糊搜文件内容 fg() { local file file=$(rg --line-number --no-heading --color=always "${1:-.}" \ | fzf --ansi --delimiter=':' \ --preview 'bat --color=always {1} --highlight-line {2}' \ | cut -d':' -f1) [[ -n "$file" ]] && ${EDITOR:-vim} "$file" } # fzf-git-log:浏览 git log fgl() { git log --oneline --color=always | fzf --ansi \ --preview 'git show --color=always {1}' \ --bind 'enter:execute(git show {1} | less -R)' } ``` ## 与其他工具组合 ### 配合 ripgrep + bat ```bash export FZF_DEFAULT_COMMAND='rg --files --hidden --glob "!.git"' export FZF_DEFAULT_OPTS=' --height 40% --layout=reverse --border --preview "bat --color=always --style=numbers --line-range=:500 {}" ' ``` 放 `~/.bashrc`,之后 Ctrl-T 直接带预览。 ### fzf + Neovim 最快的"在 vim 里 fuzzy 找文件": ```vim " init.lua / vimrc nnoremap <leader>f :Telescope find_files<cr> " telescope 已经是 fzf-like ``` 或装 `fzf.vim`: ```vim nnoremap <leader>f :FZF<cr> ``` ## 多选 + 操作 ```bash # 多选文件批量删除 rm $(fzf -m) # 多选 git commit 做 cherry-pick git cherry-pick $(git log --oneline | fzf -m | awk '{print $1}') ``` `-m` 启用 multi-select;用 Tab 选中 / 取消选中。 ## 自定义 preview ```bash # 选目录,右边显示 ls find . -type d | fzf --preview 'ls -la {}' # 选 PR 号,右边显示 PR 详情 gh pr list | fzf --preview 'gh pr view {1}' | awk '{print $1}' ``` ## 模糊语法 - 默认 fzf 用 "smart case + fuzzy":你打 `usrlist` 能匹配 `User_List_View.tsx` - `'word`:精确匹配 - `^prefix`:开头 - `suffix$`:结尾 - `!noword`:取反 - `word1 | word2`:或 ## 自定义颜色 ```bash export FZF_DEFAULT_OPTS=' --color=bg+:#313244,bg:#1e1e2e,spinner:#f5e0dc,hl:#f38ba8 --color=fg:#cdd6f4,header:#f38ba8,info:#cba6f7,pointer:#f5e0dc --color=marker:#f5e0dc,fg+:#cdd6f4,prompt:#cba6f7,hl+:#f38ba8 ' ``` Catppuccin Mocha 主题。其他主题:fzf 的 wiki 一大堆。 ## 把 fzf 放进生产脚本 ```bash #!/usr/bin/env bash # deploy.sh - 选要部署的版本 TAG=$(git tag | sort -rV | fzf --header='选要部署的版本') || exit 1 echo "Deploying $TAG..." ssh prod-server "cd /srv/app && git fetch && git checkout $TAG && systemctl restart app" ``` CI 里 fzf 没意义(无 tty),但本地交互式脚本用 fzf 比一堆 case 语句 干净得多。 ## 踩过的坑 - 嵌套 fzf(fzf 选完结果当 fzf 参数)容易卡住——子 fzf 进程被父 fzf 抢 tty。中间存个变量再调用。 - `FZF_DEFAULT_COMMAND` 设了 `rg` 但 rg 没装:所有 fzf 调用都报错。 确保 rg / fd 都装了。 - 大目录(node_modules、.cache)拖慢 fzf:默认 command 加 `--glob '!**/node_modules'` 之类排除。fd 比 find 快 5-10 倍且默认尊重 .gitignore。 - WSL 里 fzf 偶尔光标错位,是终端 escape sequence 问题;升级 Windows Terminal + 用最新 fzf 解决。

GitHub Actions:matrix build / cache / reusable workflow 实战

## 起因 库需要在 Python 3.10/3.11/3.12 × Linux/Mac/Windows × 9 个组合上测试。 之前老 CI 写 9 个 job 重复代码。GitHub Actions 的 matrix 一行配齐。 ## 1. matrix build ```yaml # .github/workflows/test.yml name: test on: push: branches: [main] pull_request: jobs: test: strategy: fail-fast: false matrix: python: ['3.10', '3.11', '3.12'] os: [ubuntu-latest, macos-latest, windows-latest] exclude: - os: windows-latest python: '3.10' # 跳某些组合 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} cache: 'pip' - run: pip install -e .[dev] - run: pytest -xvs ``` `fail-fast: false`:一个 job 失败不取消其它(看完所有再 fail)。 8 个 job 并行跑(GitHub 免费 plan 公开 repo 20 并发)。 ## 2. cache 加速 ```yaml - uses: actions/cache@v4 with: path: | ~/.cache/pip ~/.cache/pypoetry .venv key: ${{ runner.os }}-py${{ matrix.python }}-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-py${{ matrix.python }}- ``` `key` 包含 lockfile hash → lockfile 没变 → cache 命中。 `restore-keys` fallback:完全不命中时拿前缀匹配最新的(部分有比没有强)。 cache 命中跳过 pip install → CI 时间从 2 分钟 → 20 秒。 Node: ```yaml - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci ``` setup-node action 内置 npm/yarn/pnpm cache,不需要单独 actions/cache。 ## 3. matrix + variables ```yaml strategy: matrix: include: - { name: 'python 3.10 / no extras', python: '3.10', extras: '' } - { name: 'python 3.12 / all extras', python: '3.12', extras: 'all' } - { name: 'python 3.12 / pgsql', python: '3.12', extras: 'postgres' } runs-on: ubuntu-latest steps: - run: pip install -e .[${{ matrix.extras }}] - run: pytest ``` `include` 比 `python: × extras:` 笛卡尔积更精细,只跑你 list 的组合。 ## 4. reusable workflows 公共逻辑抽到一个 yml 给多 repo 用: ```yaml # .github/workflows/python-test.yml (reusable) name: python-test on: workflow_call: inputs: python-version: type: string default: '3.12' coverage: type: boolean default: false secrets: CODECOV_TOKEN: required: false jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - run: pip install -e .[dev] - run: pytest ${{ inputs.coverage && '--cov' || '' }} - if: inputs.coverage uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} ``` 调用方: ```yaml # 某 repo .github/workflows/ci.yml jobs: test: uses: my-org/.github/.github/workflows/python-test.yml@main with: python-version: '3.12' coverage: true secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ``` 更新 reusable yml → 所有调用方下次 run 自动用新版(无需逐个 repo 改)。 ## 5. composite actions(细粒度复用) 复用几个 step 而不是整个 workflow: ```yaml # my-action/action.yml name: 'Setup project' description: 'Install uv + Python + sync deps' inputs: python-version: default: '3.12' runs: using: composite steps: - uses: astral-sh/setup-uv@v3 with: version: 'latest' enable-cache: true - uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - run: uv sync --frozen shell: bash ``` ```yaml # 调用 - uses: ./my-action with: python-version: '3.12' - run: uv run pytest ``` 放在仓库本地 (`./.github/actions/setup`) 或独立 repo (`my-org/setup-action@v1`)。 ## 6. 条件运行 ```yaml - if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: ./scripts/deploy.sh - if: matrix.os == 'ubuntu-latest' run: ./scripts/coverage.sh - if: failure() run: ./scripts/notify-slack.sh ``` `success() / failure() / always() / cancelled()` 是 step 级条件。 ## 7. 路径 / 分支过滤 ```yaml on: push: paths: - 'backend/**' - '.github/workflows/backend.yml' branches: - main - 'release/**' ``` monorepo 里只改前端时不跑后端 CI。 ## 8. concurrency 防同一分支同时多 run ```yaml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true ``` PR 连续 push 时,老的 in-progress run 被 cancel,只跑最新。 省 CI 资源。 ## 9. secrets / env ```yaml env: NODE_ENV: production API_BASE_URL: https://api.example.com jobs: deploy: env: EXTRA: value steps: - env: STEP_LOCAL: x run: echo "$EXTRA $STEP_LOCAL ${{ secrets.AWS_KEY }}" ``` secrets 在 repo settings → Secrets and variables → Actions 配。 环境(environment)配的可加 approval gate: ```yaml jobs: deploy-prod: environment: production # 需要审批员点击 approve 才跑 steps: ... ``` ## 10. 自托管 runner(cost / hardware 控制) ```yaml runs-on: [self-hosted, linux, gpu] ``` 自己机器装 GitHub Actions runner agent → label 标记 → workflow 用 label 选择。免费层时间紧 / 需 GPU / 需大内存时省钱。 ## 11. 经验:调试 workflow ```bash # 本地跑 workflow(用 act) brew install act act -j test # 跑 test job act pull_request # 模拟 pr 事件 ``` act 用 Docker 模拟 GitHub Actions 环境。不 100% 一致(缺一些 GitHub service),但能本地快速迭代。 或者用 `tmate` 在 GitHub runner 里开 SSH 会话调试: ```yaml - name: SSH into runner if test fails if: failure() uses: mxschmitt/action-tmate@v3 with: detached: true limit-access-to-actor: true ``` step 失败时 GitHub 给你 SSH URL,进 runner 看现场。强烈推荐紧急调试用。 ## 12. cost 控制 - GitHub 免费层公开 repo 无限 minutes;私有 repo 每月 2000 minutes - 用 `concurrency` cancel 老 run - 选小机器(默认 ubuntu-latest 2-core;linux 私有可以选 `ubuntu-24.04-arm` 64 cores 但贵 16x) - 把 release / nightly build 放 self-hosted ## 效果 我们一个开源库 CI: - matrix build 9 组合并行,总时长 4 分钟(顺序跑要 30 分钟+) - cache 让 build 时间 80% 来自实际跑测试,不是装依赖 - reusable workflow 让 6 个相关 repo 共享配置,改一处生效全部 - composite action 把"setup uv + sync"封装,每 yml 少 10 行 ## 踩过的坑 1. **cache key 不带 lockfile hash**:依赖变了 cache 还用老的, bug 隐蔽。永远 `hashFiles('package-lock.json')` 之类。 2. **secrets 在 PR 不可见**:跨 fork PR 默认不传 secrets(防泄露)。 要 dependabot / fork PR 跑需要 deploy → 用 `workflow_run` triggered workflow 在 base repo 跑。 3. **windows runner 路径分隔**:脚本里 hardcode `/` 在 Windows 上 失败。用 cross-platform shell(bash 在 Windows runner 也有)+ `\` `/` 都用 `path.join`。 4. **timeout 默认 6 小时**:偶尔 hang 的 job 把 minutes 烧光。 `timeout-minutes: 30` 给每个 job 设上限。 5. **actions 升级 v3 → v4 breaking**:node 16 退役大批 actions 强制 升 node 20。看 GitHub deprecation notice + dependabot for actions 自动 PR 升级。

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 重来。