#!/usr/bin/env bash
#
# pre-push：**推送前**拦住"不该进版本库的文件"。
#
# ★ 为什么要有这个钩子，而不是只靠 `criteria-hygiene` 里那条判据：
#   那条判据读的是 **index**（`git ls-files`），它守的是"**不会被再加回来**"，
#   不是"**不会被推出去**"。而 blob 恰恰躺在**未推送的提交**里 ——
#   也就是说 `git rm --cached` 之后，东西已经从 index 里消失了，
#   可是**历史里还有**，下一次 `git push` 会连它一起发出去，而**没有任何东西会红**。
#   ⇒ "先 ignore + `rm --cached`、再 push"这条顺序**只活在提交信息里**，
#     而"只活在文字里的规则等于没有规则"正是我们加那条判据的理由。
#     判据是**泄露之后**响的闹钟；这个钩子是**不让它泄露**。两件事都要有。
#
# 这个钩子由 `deploy/install.sh --git-hooks` 装（`core.hooksPath` 指向 `.githooks`），
# 所以它**跟着仓库走**：换一台机器 clone 下来，装一次就都装上了。
#
# 退出码：0 放行，1 拦下（git 会中止 push）。**绝不返回 2** ——
# 钩子里非 0 一律中止推送，所以"钩子自己坏了"与"真的有违规"都会拦下来，
# 这是**故意的方向**：宁可推不出去，也不要静默推出去。
set -uo pipefail

# 不许进版本库的路径（**加新路径就加在这里**，别写进判据里到处复制）
FORBIDDEN=(
  "client/harmony/entry/src/main/resources/rawfile/agconnect-services.json"
)

# 生效范围：推送的所有 ref 区间。钩子从 stdin 收到：
#   <local ref> <local sha> <remote ref> <remote sha>
# 远端 sha 全 0 = 新分支 ⇒ 要查**整个可达历史**。
zero="0000000000000000000000000000000000000000"
declare -a RANGES=()
while read -r _lref lsha _rref rsha; do
  # ★ **删除**用**全零** sha 表示，不是空串（原来是 `[ -z "${lsha:-}" ] && continue`）。
  #   少了这一支的后果：删除时 lsha=全零、rsha=旧值 ⇒ 走 else ⇒ 拼出
  #   `旧sha..0000…` ⇒ `git log` 解析不了 ⇒ **exit 1 中止**。
  #   实测（真 bare 仓）：
  #     $ git push /tmp/bare-del.git --delete tmp
  #     pre-push: 无法解析范围 '6702cc2f…..0000000000000000000000000000000000000000' —— 中止推送
  #
  #   为什么这不是小事：**删除不发布任何东西**，所以这是**纯误红**；
  #   而它恰恰因为"宁可推不出去"这条**正确的**方向而不可见 —— 看起来就像设计好的安全行为。
  #   同族形状：**一道闸消费了不属于它管辖的东西**（"到期闸把汇总一起吃掉"也是这个）。
  #   附带效果：`install.sh --check` 会一直说"钩子已接"，而实际上删分支/删 tag 全被挡。
  #
  #   ★ 我第一步只验了"会泄露的范围 ⇒ 1、已推过的范围 ⇒ 0"——
  #     **两个方向都没覆盖"没有东西被发布"这一支**，而它正是坏的那一支。
  #     和"只判下界"是同一条：**把边界少判了一侧**。
  if [ -z "${lsha:-}" ] || [ "$lsha" = "$zero" ]; then
    continue        # 没有东西被发布（删除 / 空 ref）—— 放行
  fi
  if [ "$rsha" = "$zero" ]; then
    # 新分支/新 tag：从根开始查（`--not --remotes` 会漏掉已被别的远端包含的情况）
    RANGES+=("$lsha")
  else
    RANGES+=("${rsha}..${lsha}")
  fi
done

if [ "${#RANGES[@]}" -eq 0 ]; then
  exit 0   # 没有任何 ref 要推（例如只删远端分支）—— 放行
fi

problems=0
for path in "${FORBIDDEN[@]}"; do
  for range in "${RANGES[@]}"; do
    # `--all` 覆盖"这次推送带出去的所有提交里，这个路径是否被改动过"
    if ! hits="$(git log --format='  %h %s' "$range" -- "$path" 2>/dev/null)"; then
      echo "pre-push: 无法解析范围 '$range' —— 中止推送（宁可推不出去，也不要盲推）" >&2
      exit 1
    fi
    if [ -n "$hits" ]; then
      problems=1
      {
        echo ""
        echo "✗ 这次推送会把 **$path** 的历史一起发出去。"
        echo "  它是 AGC 客户端凭证，而本仓的镜像是**匿名可 clone 的公开项目**。"
        echo "  涉及这些提交："
        echo "$hits"
        echo ""
        echo "  怎么处置（按顺序）："
        echo "    1) 如果这些提交**已经**推上去过：按\"已泄露\"处理 —— 去 AGC 轮换，"
        echo "       并且**先**在本地把这些提交改掉（rebase/filter-repo），再推。"
        echo "    2) 如果还没推过：把涉及它的提交从待推范围里摘掉，或者先把历史里的 blob 清掉。"
        echo "    3) 确认它**不在**要推的范围里之后，再 push。"
        echo ""
        echo "  （这条钩子的存在理由：判据读的是 index，只证明\"不会再被加回来\"；"
        echo "    而 blob 还躺在未推送的提交里，只有推送前拦得住。）"
      } >&2
    fi
  done
done

# ── 第二道：构建产物里内嵌 AGC 真身（2026-09-28 新增）────────────────────
#
# 为什么不能只靠上面的路径清单：**hap/ipa 之类的打包产物会把
# `agconnect-services.json` 原封不动打进去**，而它在产物里只是一个 zip 条目。
# 逐条列举路径追不上产物形态（下次换个构建目标又漏一次）。
#
# 实测（`git ls-files` 抓到的）：两个 36MB 的 .hap，内含
# `resources/rawfile/agconnect-services.json`，其中 client.client_id /
# code.code1..4（4 个 32 位十六进制）/ oauth_client.client_id 全是真值。
#
# 口径：只看**本次推送范围内被新增/修改的二进制产物**，不扫全历史 ——
#   ① 全历史扫描在大仓上很慢，而这道闸的目标是"拦住下一次手滑"；
#   ② 已经推出去的那部分由 `criteria-hygiene` 那条报警负责（泄露之后响的闹钟）。
#
# ★ 这里用 `git log --diff-filter=A --name-only` 而不是 `git diff`（2026-09-28 实测踩坑）：
#   `RANGES` 里的元素是「`rsha..lsha`」或（新分支时）**单个 `lsha`**。
#   把单个 sha 传给 `git diff` 并不会得到"这个范围里新增的文件"—— 它是
#   「工作区 vs 该 sha」的差异，**在干净工作区上恒为空** ⇒ 判据**静默失效**，
#   而失效的判据与通过的长得一模一样。（第一版就这么写的，实测：含凭证的 hap 被放行，
#   push exit=0。）
#   `git log` 才是"这些提交里动过什么"的那个读数，与第一道检查同源。
#
# 判据形态：**新增或修改的构建产物** + 在该提交的文件内容里搜
#   `agconnect-services.json` 这个**条目名**。用条目名而不是凭证值本身，
#   因为值会变而条目名稳定，且搜值需要把凭证读进内存。
for range in "${RANGES[@]}"; do
  if ! bins="$(git log --diff-filter=AM --name-only --format= "$range" -- '*.hap' '*.app' '*.ipa' 2>/dev/null)"; then
    echo "pre-push: 无法解析范围 '$range'（查构建产物）—— 中止推送" >&2
    exit 1
  fi
  bins="$(printf '%s\n' "$bins" | sort -u | grep -v '^$')"
  [ -z "$bins" ] && continue
  while IFS= read -r b; do
    [ -n "$b" ] || continue
    # ★★ 取 blob 必须先把范围解析成**单个 commit**，再 `ls-tree`（2026-09-28 实测两次踩坑）：
    #
    #   ① `git rev-parse "a..b:path"` 输出**两行**（blob sha + `^a`），
    #      赋进变量是多行值，`cat-file` 失败而被 `||` 兼掉 ⇒ 静默放行。
    #      （手工喂真实待推范围给钩子，exit=0；而同一范围内 `git log` 明确列出了两个 hap。
    #       判据"看起来在检查"而实际没执行 —— 与本仓 python-probe-shadowing 那笔同族。）
    #   ② 改用 `ls-tree` **也报错**：它不接受范围
    #      （`fatal: Not a valid object name a..b`）。
    #
    #   ⇒ 正确形状：先取范围内**最后一个** commit（`git rev-list --max-count=1`），
    #     再 `ls-tree <那个 commit>`。第一道检查用的是 `git log`（天生吃范围），
    #     所以它没踩这个坑 —— **同一条数据，git 子命令的输入契约不一样**。
    tip="$(git rev-list --max-count=1 "$range" 2>/dev/null | head -1)"
    if [ -z "$tip" ]; then
      echo "pre-push: 范围 '$range' 解析不出 commit（查构建产物）—— 中止推送" >&2
      exit 1
    fi
    blob_sha="$(git ls-tree "$tip" -- "$b" 2>/dev/null | awk 'NR==1{print $3}')"
    if [ -z "$blob_sha" ]; then
      # 该 tip 上没有这个文件（可能在范围内被删了）—— 读不到就不当违规，但**说明**，
      # 因为"没检查到"与"没问题"不是一回事。
      echo "pre-push: 读不到 $b 在待推范围里的内容（可能已被删除）—— 未据此判红" >&2
      continue
    fi
    # ★★ **必须扫全文件**（2026-09-28 实测修正）：原先只取头尾各 4MB，
    #   理由是"zip 的中央目录在尾部"。实测本仓真 hap 里那个条目在
    #   **byte 28,239,718 与 35,958,749**（文件共 35MB）—— 头 4MB 扫不到、
    #   尾 4MB 也只勉强够到第二处。**第一版会把这个含真凭证的 hap 放过去。**
    #   ⇒ 改全量流式扫（grep -a 直接吃管道，不进内存）。
    #
    #   代价：每个 35MB 产物要读一遍。推送路径上、非热路径，可以接受。
    #   真的嫌慢就改成 `grep -a -m1`（找到一个就停），语义不变且更快。
    # ★★★ **不能用 `grep -q` 接管道**（2026-09-28 实测，方向与直觉相反）：
    #   `grep -q` 找到就**立即退出**，于是上游 `git cat-file` 在写管道时收到 SIGPIPE
    #   ⇒ 它的退出码是 **141**。而本钩子开头有 `set -uo pipefail`（`pipefail` 把管道
    #   判成取**各段非零的合取**）⇒ 整条管道变成 141 ⇒ `if` 判为**假**。
    #
    #   实测读数：命中时 `PIPESTATUS=141 0`（cat-file=141, grep=0）⇒ `if` 不成立。
    #   **即"找到了凭证"被读成"没找到"** —— 而失效方向恰好是**放行**，
    #   与"宁可推不出去"的正确方向相反，是最坏的那种失效。
    #
    #   ⇒ 不用 `-q`：让 grep 读完整条流、正常退出 0，最后一个命令自然是 grep 的码。
    #   （代价是扫完整个 35MB 而不是命中即停；推送路径上可接受。）
    if git cat-file -p "$blob_sha" 2>/dev/null | grep -a 'agconnect-services\.json' >/dev/null; then
      problems=1
      {
        echo ""
        echo "✗ 这次推送会带上构建产物 **$b**，而它内嵌了 agconnect-services.json。"
        echo "  那是 AGC 客户端凭证（api_key 信封），而本仓镜像是**匿名可 clone 的公开项目**。"
        echo ""
        echo "  怎么处置："
        echo "    1) 构建前把 AGC 真身从打包输入里去掉（用 .example.json 占位），"
        echo "       或在构建产物生成后立刻 git rm --cached 掉。"
        echo "    2) 已经进了要推的提交：用 filter-repo / rebase 把该 blob 清掉，"
        echo "       确认这些提交**没有**被推过；推过了就按已泄露处理 —— 去 AGC 轮换。"
        echo ""
        echo "  （这一格是**按内容**查的，不是按路径 —— 产物名会变，凭证的容器也会变。）"
      } >&2
    fi
  done <<<"$bins"
done

exit "$problems"
