---
date: 2026-05-10
updated: 2026-05-19
tags: [git, branch-hygiene, patch-id, zombie-branch, squash-merge]
---

# 判断"本地分支领先 origin"到底是真未合还是僵尸：先用文件 diff，patch-id 只在 ff/rebase 合并下可靠

## 场景

`git for-each-ref` 显示某个本地分支「领先 origin/X N 个 commit」，常规直觉：未推送的工作。但本项目（**默认 squash 合并 + auto-delete-on-merge**）下，这种"领先"经常是僵尸——commits 早通过别的分支或同分支 squash 合掉了，远端分支被自动删，本地留下一串 SHA 找不到归属。直接 push 会重建 stale 远端分支浪费 review。

## ⚠️ 项目默认 squash，patch-id 检测靠不住

`feature/* → develop` 默认 **squash**（CLAUDE.md「环境升级」段）。squash 把 N 个原 commit 合成 1 个，**整个 patch 变了**，逐 commit patch-id 永远找不到等价。同理 `merge-base --is-ancestor` 也失败（SHA 全新）。

→ **patch-id 法只在 fast-forward / rebase-merge / per-commit cherry-pick 这种"每个 commit 单独保留到 develop"的场景可靠**。项目主线（feature → develop squash + env promote FF-only）刚好 100% squash 主线，patch-id 在主线**根本用不了**。

## ✅ 真正可靠的判定：文件级内容 diff

```bash
# 步骤 0（必做）：先 fetch，本地 ref 过期是另一个常见坑
git fetch origin --prune

# 步骤 1（金标准）：文件级 diff，看分支独有 commit 改的文件在 develop 上是不是同样内容
#   选一个该分支改过的关键文件，直接读 develop 上的内容跟本地分支 HEAD 比
git show origin/develop:path/to/changed-file.ts > /tmp/dev.ts
git show feat-branch:path/to/changed-file.ts > /tmp/br.ts
diff /tmp/dev.ts /tmp/br.ts

# 同样的内容 → 已合（即使 git ancestry 说 "ahead N"）
# 不同 → 真的有未合内容
```

如果分支改过多个文件，全 diff：
```bash
git diff origin/develop..feat-branch -- $(git log origin/develop..feat-branch --no-merges --name-only --format= | sort -u)
```

## 辅助：从 PR 这一侧反查

PR auto-delete 后 Gitea API 的 `head.ref` 退化成 `refs/pull/N/head`——**原分支名信息丢失**，按 `head.ref` 包含 "agent" 这种关键词过滤会漏掉已合 PR。

正确做法：按 **PR title 搜关键词**，或者直接搜 `merge_commit_sha` / `merged_at` 时间窗：
```bash
# 用 title 搜（branch name 不可靠）
curl ... /pulls?state=closed&limit=100 | jq '.[] | select(.title | test("Memory M1"))'

# 或者按 commit 文案直接 grep develop 历史
git log origin/develop --oneline --grep="Memory M1"
```

## 检测流程（已废 patch-id 三连，替换为 squash-aware 版）

```bash
# Step 0: fetch
git fetch origin --prune

# Step 1: tip 是否已是 develop 祖先（fast-forward / rebase merge 才有效）
git merge-base --is-ancestor "$br" origin/develop && echo "✅ 已合（ancestor 法）" && exit 0

# Step 2: 在 develop 历史里按 commit subject 找 squash commit
for sha in $(git log "origin/develop..$br" --no-merges --format='%H'); do
  subj=$(git log -1 --format='%s' "$sha")
  found=$(git log origin/develop --oneline --grep="$subj" | head -1)
  [ -n "$found" ] && echo "⚠️  '$subj' 可能已通过 squash 合入: $found"
done

# Step 3: 金标准——文件级 diff
files=$(git log "origin/develop..$br" --no-merges --name-only --format= | sort -u | head -5)
for f in $files; do
  if git diff --quiet "origin/develop:$f" "$br:$f" 2>/dev/null; then
    echo "  $f  ✓ 已同步"
  else
    echo "  $f  ✗ 内容有差异"
  fi
done
```

## 实战样本

### 5/10：weekly-retro-skill-orchestrator（fast-forward 历史，patch-id 法成功）
作者在原分支提 commit，转去新分支 `docs/weekly-retro-architecture` 提 PR #281。patch-id 命中。**这是 patch-id 法适用的场景**。

### 5/19：feature/ffai-agent-optimization（squash 历史，patch-id 法失败 → 文件 diff 救场）
分支领先 develop 13 个 non-merge commit，全部经 PR #423 squash 合到 develop 的 `f5d43d66`。
- ancestry 法：失败（SHA 全新）
- patch-id 法：失败（5 commit 合 1，patch 改写）
- API 按 head.ref 过滤 "agent"：漏掉（auto-delete 后 head.ref = `refs/pull/423/head`）
- **文件 diff 法**：一行结案——`git show origin/develop:memories.controller.ts | grep 'TODO(#432)'` 已存在 → 全部已合
- 浪费时间根因：连续用 3 个 SHA-based 工具，没第一时间 diff 文件内容

## 元规则

1. **AI 节奏下用户的"我记得已合"通常比 AI 重新推理更准**——记忆 vs 推理冲突，先用文件 diff 裁判，不要继续 API/git ancestry 查找
2. **判"已合 vs 未合"的金标准永远是文件级内容比对**，SHA / patch-id / API 过滤都是辅助
3. **第一性原理**：要回答的问题是"这些代码改动是否已经在 develop 上"——直接读文件最直接，绕弯走 ancestry 都是 indirection
4. **本地 ref 永远先 fetch 再判断**，否则比较基准就是错的

## 适用范围

任何长期开发分支 / 复用的 worktree / squash-merge 项目（绝大多数 GitHub flow / Gitea flow 默认配置）。`[ahead N]` 看到时不要直接 push 也不要直接信 patch-id 检测，先 fetch + 文件 diff。

## 关联

- `.learnings/2026-05-15-test-fabricate-squash-merge.md` — squash 后 SHA 改写的另一个表现
- `.learnings/ERRORS/ERR-20260518-001-slot-reclaim-drift.md` — slot 重 claim 漂移（不同主题但同样"看着是 X 实际是 Y"的判定陷阱）
- CLAUDE.md「Git 规则」段：feature/* → develop = squash 是项目默认
