PR 流程 SOP

第一性原理:PR base 必须是 upstream/main

GitHub 计算 PR diff 是从 fork head 仓库的 branch 出发对比到 base 仓库的 branch。如果 base 选成 fork main(Diraw:main)或本地分支,GitHub 会从 fork main → upstream main 的 merge-base 算起——所有 fork 独有的 commit 都会被算进 PR diff

正确姿势:

base repository:   Yuan-lai-ru-ci/ProferAI   ← 上游
base branch: main
head repository: Diraw/ProferAI ← fork
compare branch: feat/<name>

新功能 PR 6 步

# 1. 同步 upstream
git fetch upstream main

# 2. 新建分支(必须基于 upstream/main,不是本地 main 也不是 origin/main)
git checkout -b feat/<name> upstream/main

# 3. 开发(改文件 + typecheck + test)

# 4. Commit(长 message 用 -F <file> 避开 bash 引号嵌套爆炸)
git add <files>
git commit -F .workbuddy/scratch-<pr>/msg.txt

# 5. Push 到 origin
git push -u origin feat/<name>

# 6. GitHub 开 PR,base = Yuan-lai-ru-ci:main

本地 main 同步

git fetch upstream main
git fetch origin main # 可选,看 fork 同步状态
git checkout main
git merge --ff-only upstream/main # 必须以 upstream 为 base,不要 merge origin/main

为什么不能 merge origin/main

  • Diraw fork 的 main 通过 GitHub “Sync fork” 跟着 upstream 同步,但有延迟
  • merge origin/main 等于把 fork 那份陈旧镜像吃进本地 main
  • 下次开 feat 分支基于本地 main 时,那批陈旧 commit 又会被算进 PR diff

长 commit message 的写法

# ❌ 多行 + 中文双引号 → bash 引号嵌套爆炸
git commit -m "fix: 修复 X

"也修 Y""

# ✅ 写到文件用 -F
cat > .workbuddy/scratch-<pr>/msg.txt <<'EOF'
fix: 修复 X

"也修 Y"
EOF
git commit -F .workbuddy/scratch-<pr>/msg.txt

cherry-pick 多 commit SOP

触发场景

把一个旧分支上的多个 commit 重放到新 base 上:

  • 旧分支基于 upstream@old-tip,要基于 upstream@new-tip 重做
  • 旧分支混入了 fork 无关历史,需要剔除后重建
  • PR review 后要 rebase 但不想用 rebase(rebase abort 会触发 ref 写坏)

三步走标准做法

# 第 1 步:working tree 同步到新 base
git reset --hard <new-base>

# 第 2 步:删 untracked 文件(reset --hard 不删 untracked,但 cherry-pick 要新建时会冲突)
rm <untracked-files-created-by-cherry-pick>

# 第 3 步:一次列出全部 commit,按顺序 apply
git cherry-pick <hash1> <hash2> <hash3>

untracked file 冲突根因

git reset --hard 改的是 tracked files,untracked files 不会动。新 base 上没有 HistoryDrawer.tsx / conversation-manager.test.ts 这些 cherry-pick 要新建的文件,但 working tree 里有它们(之前旧分支留下)→ cherry-pick 报 “would be overwritten”。

处理:

# 冲突时先 abort
git cherry-pick --abort

# 删掉冲突的 untracked 文件
rm HistoryDrawer.tsx conversation-manager.test.ts

# 重 cherry-pick
git cherry-pick <hash1> <hash2> <hash3>

cherry-pick hash ≠ merge hash

PR 合并进 upstream 后:

git branch -d feature/<name>
# error: The branch 'feature/<name>' is not fully merged.

根因:cherry-pick 重放了文件内容但创建了新的 commit 对象,本地 hash(如 576d7bfd)跟 PR 在 upstream main 上的 merge commit hash(如 15ef1d30)不一样。git branch -d 看本地 hash 不在 upstream history 就拒绝删。

正确做法:内容已合 upstream,直接 git branch -D

vs git rebase

cherry-pick rebase
风险 低(只 apply 选定 commit) 高(重写整条历史)
中途 abort 不破坏 ref rebase abort 在 Git for Windows 上会触发 packed-refs bug,导致 ref 全废
推荐场景 多 commit 跨 base 重放 单分支线性 history 整理

默认走 cherry-pick,只在确实需要清理单分支历史时才用 rebase。


SOP 速查表

场景 错误做法 正确做法
PR base Diraw:main 或本地分支 Yuan-lai-ru-ci:main
新建分支 基于本地 main 或 origin/main git checkout -b feat/<name> upstream/main
同步本地 main merge --ff-only origin/main merge --ff-only upstream/main
长 commit message -m "多行中文..." -F <file>
cherry-pick 多 commit 直接 cherry-pick 报冲突 reset --hard <new-base> + rm <untracked> + cherry-pick 全部
untracked 冲突处理 强行 git add 解决 abort + rm + 重 cherry-pick
删已合并分支 git branch -d 因 hash 不匹配失败 git branch -D
重放多 commit git rebase(风险高) git cherry-pick <多个 hash>
PR 想 “改 commit message” git reset git commit-tree <原-tree> -p <parent> -m "新 message" 重写 commit,不动 ref