Why: the make-variable pass-through re-quoted CMD and broke any command containing quotes (caught by the redaction self-test before it could run). Evidence: redaction self-test now records 'token <redacted> hello' with quoting intact (row sha256 aae6a895); negative 'false' case earlier recorded exit=1 row and propagated rc — both in the task card evidence table. Impact: CMD now travels via environment (CMD="..." make evidence); SRC/TAIL stay make variables; usage strings updated in Makefile/AGENTS.md/task.sh template comment. Refs: task card evidence table; guidelines draft.10 tool-written-evidence rule.
400 lines
22 KiB
Bash
Executable File
400 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 任务启动门禁:precheck / start / sync / close / finish(配套 Makefile 同名目标)。
|
||
# 规范依据:collaboration.md 共同基线与同步节点、治理清理;delivery.md §1/§5。
|
||
set -euo pipefail
|
||
|
||
TARGET_BRANCH="${TASK_TARGET_BRANCH:-main}"
|
||
|
||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||
need_repo() { git rev-parse --git-dir >/dev/null 2>&1 || die "not inside a git repository"; }
|
||
width() { python3 -c 'import sys,unicodedata;print(sum(2 if unicodedata.east_asian_width(c) in "WF" else 1 for c in sys.argv[1]))' "$1"; }
|
||
|
||
cmd_precheck() {
|
||
need_repo
|
||
git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true
|
||
local branch head rmain dirty sub_drift sub_uninit
|
||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||
head=$(git rev-parse HEAD)
|
||
rmain=$(git rev-parse --verify -q "origin/${TARGET_BRANCH}" 2>/dev/null || echo "$head")
|
||
dirty=$(git status --porcelain | { grep -v '^.. \.zcode/' || true; } | wc -l)
|
||
sub_drift=$(git submodule status --recursive 2>/dev/null | { grep -cE '^\+|^U' || true; })
|
||
sub_uninit=$(git submodule status --recursive 2>/dev/null | { grep -cE '^-' || true; })
|
||
local behind ahead
|
||
behind=$(git rev-list --count "HEAD..origin/${TARGET_BRANCH}" 2>/dev/null || echo 0)
|
||
ahead=$(git rev-list --count "origin/${TARGET_BRANCH}..HEAD" 2>/dev/null || echo 0)
|
||
echo "[repo] branch=${branch} head=$(echo "$head" | cut -c1-10)"
|
||
echo "[remote] origin/${TARGET_BRANCH}=$(echo "$rmain" | cut -c1-10) ahead=${ahead} behind=${behind}"
|
||
echo "[tree] $([ "$dirty" -eq 0 ] && echo clean || echo "dirty(${dirty})") submodules_drift=${sub_drift} uninit=${sub_uninit}"
|
||
local wt merged
|
||
wt=$(git worktree list | tail -n +2 | wc -l)
|
||
merged=$(git branch --merged "origin/${TARGET_BRANCH}" 2>/dev/null | { grep -vE 'main|master|\*' || true; } | tr -d ' +')
|
||
[ "${sub_uninit:-0}" -gt 0 ] && echo "[warn] ${sub_uninit} 个子模块未初始化——构建/测试前需物化(不阻断写入)"
|
||
echo "[worktrees] linked=${wt}"
|
||
[ -n "$merged" ] && echo "[recycle] merged un-recycled branches: $(echo $merged)"
|
||
local open_cards="" pc pb pst
|
||
for pc in .task/*.md; do
|
||
[ -f "$pc" ] || continue
|
||
pb=$(card_of_branch "$pc"); [ -n "$pb" ] || continue
|
||
pst=$(card_status "$pc"); case "$pst" in 已完成*) continue ;; esac
|
||
card_pending "${pc##*/}" && continue
|
||
if git log --merges --format='%s' "origin/${TARGET_BRANCH}" 2>/dev/null | grep -qF " from $pb into ${TARGET_BRANCH}"; then
|
||
open_cards="${open_cards} ${pc##*/}"
|
||
fi
|
||
done
|
||
[ -n "$open_cards" ] && echo "[cards] 已合入未闭合任务卡:$(echo $open_cards)——make task-close 回填归档(主检出运行)"
|
||
local done_open="" stale="" now ts ref
|
||
for pc in .task/*.md; do
|
||
[ -f "$pc" ] || continue
|
||
case "$(card_status "$pc")" in
|
||
已完成*) done_open="${done_open} ${pc##*/}" ;;
|
||
esac
|
||
done
|
||
[ -n "$done_open" ] && echo "[cards] 已完成但未归档:$(echo $done_open)"
|
||
now=$(date +%s)
|
||
while read -r ref ts; do
|
||
[ -n "$ref" ] || continue
|
||
git merge-base --is-ancestor "$ref" "origin/${TARGET_BRANCH}" 2>/dev/null && continue
|
||
if [ $((now - ts)) -gt 86400 ]; then
|
||
stale="${stale} ${ref#origin/}"
|
||
fi
|
||
done < <(git for-each-ref --format='%(refname:short) %(committerdate:unix)' "refs/remotes/origin/feature/card-close-*")
|
||
[ -n "$stale" ] && echo "[cards] 闭合分支超过 24 小时未合入:$(echo $stale)"
|
||
# [hooks] 声明≠能力:core.hooksPath 未指向 scripts/hooks 时,"钩子已启用"只是声明。
|
||
local hp; hp=$(git config core.hooksPath 2>/dev/null || true)
|
||
if [ "$hp" = "scripts/hooks" ] && [ -x scripts/hooks/pre-commit ]; then
|
||
echo "[hooks] core.hooksPath=scripts/hooks(pre-commit/post-merge 实际接线)"
|
||
else
|
||
echo "[hooks] WARN core.hooksPath='${hp:-未设置}'——本地钩子未生效,先 make setup;本检出提交将绕过本地闸(远端准入仍在)"
|
||
fi
|
||
# [facts] 外部事实登记表过期项(policy/facts.md;引用前先跑其验证命令,AGENTS.md 证据与验证门禁)。
|
||
if [ -f policy/facts.md ]; then
|
||
local fid lv ttl expired="" ftotal=0
|
||
while IFS='|' read -r _ fid _ _ lv ttl _; do
|
||
fid=$(echo "${fid:-}" | tr -d ' '); case "$fid" in F[0-9]*) ;; *) continue ;; esac
|
||
ftotal=$((ftotal+1))
|
||
lv=$(echo "$lv" | tr -d ' '); ttl=$(echo "$ttl" | tr -d ' ')
|
||
case "$lv" in [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;; *) expired="$expired ${fid}(日期格式)"; continue ;; esac
|
||
case "$ttl" in ''|*[!0-9]*) ttl=30 ;; esac
|
||
if [ $(( $(date +%s) - $(date -d "$lv" +%s) )) -gt $(( ttl*86400 )) ]; then
|
||
expired="$expired ${fid}(last=${lv},ttl=${ttl}d)"
|
||
fi
|
||
done < <(grep -E '^\| *F[0-9]+ ' policy/facts.md || true)
|
||
if [ -n "$expired" ]; then
|
||
echo "[facts] WARN 过期事实:$(echo $expired)——引用前先 make facts-verify 重验并更新登记表"
|
||
else
|
||
echo "[facts] policy/facts.md ${ftotal} 条事实均在有效期内"
|
||
fi
|
||
else
|
||
echo "[facts] WARN policy/facts.md 不存在——外部事实登记表缺失(AGENTS.md 证据与验证门禁)"
|
||
fi
|
||
echo "[required-reading] core/baseline.md, workflows/delivery.md §1$([ "$dirty" -gt 0 ] && echo ", standards/collaboration.md(树脏=他人或未收尾现场,不得写入)")"
|
||
echo "[R0] $(echo "$rmain" | cut -c1-10)(完整:$rmain)"
|
||
if [ "$dirty" -gt 0 ] || [ "${sub_drift:-0}" -gt 0 ]; then
|
||
echo "[verdict] SHARED_WORKSPACE_BUSY — 工作树不干净,不得写入;先收尾或经 owner 确认"
|
||
exit 3
|
||
fi
|
||
if [ "$behind" -gt 0 ] && [ "$branch" = "${TARGET_BRANCH}" ]; then
|
||
echo "[verdict] SYNC_REQUIRED — 本地 ${TARGET_BRANCH} 落后远端 ${behind} 提交;先 git pull --ff-only"
|
||
exit 4
|
||
fi
|
||
echo "[verdict] READY — 可建任务工作区:make task-start NAME=feature/your-branch"
|
||
}
|
||
|
||
cmd_start() {
|
||
need_repo
|
||
local name="${1:-}"; [ -n "$name" ] || die "usage: task.sh start <feature|fix|hotfix>/<slug>"
|
||
echo "$name" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || die "branch name must match (feature|fix|hotfix|release)/<slug>"
|
||
git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true
|
||
[ "$(git status --porcelain | { grep -v '^.. \.zcode/' || true; } | wc -l)" -eq 0 ] || die "dirty tree — resolve first (precheck verdict BUSY)"
|
||
[ "$(git submodule status --recursive 2>/dev/null | { grep -cE '^\+|^U' || true; })" -eq 0 ] || die "submodule drift/conflict — resolve first (precheck verdict BUSY)"
|
||
git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD || die "local ${TARGET_BRANCH} behind origin — pull --ff-only first"
|
||
local rmain slug wt mainroot
|
||
rmain=$(git rev-parse "origin/${TARGET_BRANCH}")
|
||
slug=$(echo "$name" | tr '/' '-')
|
||
# 落点须为主检出的平级目录:$PWD 推算在子目录(或任务 worktree 内)运行时会算错,
|
||
# 改用 git-common-dir 的父目录定位主检出(与 cmd_finish 同款惯用法)。
|
||
mainroot=$(dirname "$(cd "$(git rev-parse --git-common-dir)" && pwd)")
|
||
wt="$(dirname "$mainroot")/$(basename "$mainroot")-wt-${slug}"
|
||
[ -e "$wt" ] && die "worktree path exists: $wt"
|
||
git worktree add -q "$wt" -b "$name" "$rmain"
|
||
# 物化后按 submodule status 实际状态核对,不只信退出码:网络抖动或嵌套指针
|
||
# 漂移时 update 可能静默留坑(2026-09-30 实测深嵌套层带病通过,构建期才炸)。
|
||
(cd "$wt" && git submodule update --init --recursive >/dev/null 2>&1) \
|
||
|| echo "[warn] submodule update 非零退出,见下方状态核对"
|
||
local sub_drift
|
||
sub_drift=$(git -C "$wt" submodule status --recursive 2>/dev/null | grep -cE '^[-+U]' || true)
|
||
[ "${sub_drift:-0}" -gt 0 ] \
|
||
&& echo "[warn] worktree 子模块物化不完整:${sub_drift} 项漂移/未初始化——构建前先核对(git -C $wt submodule status --recursive)"
|
||
local card="$wt/.task/${slug}.md"
|
||
mkdir -p "$wt/.task"
|
||
cat > "$card" <<EOF
|
||
# ${name} 任务卡
|
||
|
||
任务主记录:<PR 链接,task-finish 时填写>;状态:进行中;owner:$(git config user.name || echo unknown)
|
||
|
||
## 基线与授权
|
||
|
||
- 权威仓:$(git -C "$wt" remote get-url origin 2>/dev/null || echo '(local)');目标分支:${TARGET_BRANCH};R0(initial_base_sha)= ${rmain}
|
||
- 工作区:${wt}(worktree);分支:${name}
|
||
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
|
||
|
||
## 目标与非目标
|
||
|
||
<预期结果、受影响入口/模块/契约;明确不做什么。>
|
||
|
||
## 同步记录
|
||
|
||
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|
||
|---|---|---|---|
|
||
| 启动 | ${rmain} | $(git -C "$wt" rev-parse HEAD) | $(date -u +%FT%TZ) |
|
||
|
||
## 证据(Evidence)
|
||
|
||
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
|
||
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|
||
|---|---|---|---|---|---|
|
||
|
||
## 结果与验证(完成后填写)
|
||
|
||
- 结果:
|
||
- 验证:<结论摘要 PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据由 make evidence 写入上方证据表>
|
||
- 影响与剩余项:
|
||
EOF
|
||
git -C "$wt" add .task && git -C "$wt" commit -q -m "Record the task card for ${slug}" && echo "[card] $card(已随分支提交)"
|
||
echo "[started] branch=${name} worktree=${wt} R0=$(echo "$rmain" | cut -c1-10)"
|
||
}
|
||
|
||
cmd_sync() {
|
||
need_repo
|
||
git fetch -q origin "${TARGET_BRANCH}"
|
||
[ "$(git status --porcelain | grep -v '^.. \.zcode/' | wc -l)" -eq 0 ] || die "dirty tree — 先显式提交;不隐式 stash(collaboration.md:11)"
|
||
git merge --no-edit "origin/${TARGET_BRANCH}"
|
||
git merge-base --is-ancestor "origin/${TARGET_BRANCH}" HEAD || die "sync incomplete"
|
||
local base head card branch slug pc
|
||
base=$(git rev-parse "origin/${TARGET_BRANCH}"); head=$(git rev-parse HEAD)
|
||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||
slug=$(echo "$branch" | tr '/' '-')
|
||
card=".task/${slug}.md"
|
||
if [ ! -f "$card" ]; then
|
||
card=""
|
||
for pc in .task/*.md; do
|
||
[ -f "$pc" ] || continue
|
||
if [ "$(card_of_branch "$pc")" = "$branch" ]; then
|
||
card=$pc
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
if [ -n "$card" ] && [ -f "$card" ]; then
|
||
printf '| 同步前验收 | %s | %s | %s |\n' "$base" "$head" "$(date -u +%FT%TZ)" >> "$card"
|
||
git add "$card"
|
||
git commit -q -m "Record the sync checkpoint" || true
|
||
# [evidence] 同步改变了被测对象:卡片证据表中旧 HEAD 的行自动失效,关键检查须重跑。
|
||
local eh stale_ev="" short_head
|
||
short_head=$(echo "$head" | cut -c1-10)
|
||
while IFS='|' read -r _ _ _ eh _ _ _; do
|
||
eh=$(echo "$eh" | tr -d ' `')
|
||
[ -n "$eh" ] && [ "$eh" != "$short_head" ] && stale_ev="$stale_ev $eh"
|
||
done < <(grep -E '^\| [0-9]{4}-[0-9]{2}-[0-9]{2}T[^|]*\|' "$card" 2>/dev/null || true)
|
||
[ -n "$stale_ev" ] && echo "[evidence] WARN 证据表含旧被测 HEAD:$(echo $stale_ev | tr ' ' '\n' | sort -u | tr '\n' ' ')(≠ 当前 $short_head)——同步后关键检查须重跑并以 make evidence 重新写入"
|
||
else
|
||
echo "[warn] 找不到分支 ${branch} 的任务卡,同步记录未写入" >&2
|
||
fi
|
||
echo "[synced] sync_base=$(echo "$base" | cut -c1-10) tested_head=$(echo "$head" | cut -c1-10) checked_at=$(date -u +%FT%TZ)"
|
||
echo "[next] 跑正式验收(make test 等),证据写入任务卡"
|
||
}
|
||
|
||
# ---- 卡片闭合(防欠账)----
|
||
# 依据:delivery.md §5 治理清理;机制化 2026-09-30 治理缺口(11 张已合入卡片未闭合)。
|
||
# 检测 .task/*.md 中"分支已合入 origin/<target> 但状态非已完成"的卡片:回填状态、主记录
|
||
# (PR 链接+合并 SHA)与闭合记录,并 git mv 到 .task/archive/。变更落点:主检出运行→
|
||
# 新建 feature/card-close-* 分支推送(集成分支受保护,不直推);任务分支内运行须 --fold
|
||
# (随当前任务 PR 合入)。合并判定用合并提交主题(分支删除后仍可追溯),不依赖分支存活。
|
||
|
||
card_of_branch() { sed -n 's/^- 工作区:.*分支:\([A-Za-z0-9._/-]\+\).*/\1/p' "$1" | head -1; }
|
||
card_status() { sed -n 's/^.*;状态:\([^;]*\).*/\1/p' "$1" | head -1; }
|
||
card_merge() { # $1=branch → "sha|subject"(该分支合入 origin/<target> 的合并提交)
|
||
git log --merges --format='%H|%s' "origin/${TARGET_BRANCH}" 2>/dev/null \
|
||
| grep -F " from $1 into ${TARGET_BRANCH}" | head -1
|
||
}
|
||
card_pending() { # $1=卡片文件名——闭合是否已在某 origin/feature/card-close-* 分支待合入
|
||
local cb
|
||
for cb in $(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/feature/card-close-*"); do
|
||
git cat-file -e "${cb}:.task/archive/$1" 2>/dev/null && return 0
|
||
done
|
||
return 1
|
||
}
|
||
card_backfill() { # $1=card $2=pr_num $3=merge_sha $4=merge_subject $5=web_base(原地改写)
|
||
python3 - "$1" "$2" "$3" "$4" "$5" <<'PY'
|
||
import re, sys
|
||
path, pr, sha, subject, base = sys.argv[1:6]
|
||
s = open(path, encoding='utf-8').read()
|
||
s = re.sub(r'状态:[^;\n]*', '状态:已完成(回填)', s, count=1)
|
||
m = re.search(r'任务主记录:([^;\n]*)', s)
|
||
if m and 'pulls/' not in m.group(1):
|
||
s = s.replace(m.group(1), f'PR #{pr} {base}/pulls/{pr}(merge {sha[:10]})', 1)
|
||
s = s.replace('## 结果与验证(完成后填写)', '## 结果与验证', 1)
|
||
s = s.rstrip('\n') + f'''
|
||
|
||
## 闭合记录(card-autoclose 回填)
|
||
|
||
- 依据:merge {sha}(PR #{pr})已包含于 origin/main,分支成果已落盘;本段为合入后回填。
|
||
- 合并主题:{subject}
|
||
- 归档:随闭合移入 .task/archive/;任务执行期的过程证据见原任务记录与 PR。
|
||
'''
|
||
open(path, 'w', encoding='utf-8').write(s)
|
||
PY
|
||
}
|
||
|
||
cmd_close() {
|
||
need_repo
|
||
local fold=0
|
||
[ "${1:-}" = "--fold" ] && fold=1
|
||
git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true
|
||
# 默认值取远端通告的 Web 面(git push 输出的 PR 建议链接);可用 TASK_CARD_WEB_BASE 覆盖。
|
||
# git-panel 前缀已失效(TLS 握手失败),默认值改为实测可用的 git-home(AGENTS.md 访问事实节)。
|
||
local web_base="${TASK_CARD_WEB_BASE:-https://git-home.u1680832.nyat.app:30319}"
|
||
local url owner repo
|
||
url=$(git remote get-url origin); url="${url%.git}"
|
||
repo=$(basename "$url"); owner=$(basename "$(dirname "$url")")
|
||
local branch c b st m cards=""
|
||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||
for c in .task/*.md; do
|
||
[ -f "$c" ] || continue
|
||
b=$(card_of_branch "$c"); [ -n "$b" ] || continue
|
||
st=$(card_status "$c")
|
||
done_unarchived=0
|
||
case "$st" in 已完成*) done_unarchived=1 ;; esac
|
||
if card_pending "${c##*/}"; then
|
||
echo "[close] ${c##*/} 已有待合入的闭合分支,跳过(等其 PR 合入)"
|
||
continue
|
||
fi
|
||
m=$(card_merge "$b") || m=""
|
||
if [ -z "$m" ]; then
|
||
[ "$done_unarchived" = 1 ] || continue
|
||
m="ARCHIVE"
|
||
fi
|
||
cards="$cards
|
||
$c|$b|$m"
|
||
done
|
||
[ -n "$cards" ] || { echo "[close] 无已合入未闭合的任务卡"; exit 0; }
|
||
# 落点:主检出→feature/card-close-* 分支(提交过 pre-commit 分支闸);任务分支须显式 --fold
|
||
local newbranch=""
|
||
if [ "$branch" = "${TARGET_BRANCH}" ]; then
|
||
[ -z "$(git status --porcelain | grep -v '^.. \.zcode/' || true)" ] || die "主检出树脏——先收尾再 close(防混入未提交现场)"
|
||
newbranch="feature/card-close-$(date -u +%Y%m%d-%H%M%SZ)"
|
||
git rev-parse --verify -q "refs/heads/${newbranch}" >/dev/null && die "分支已存在:${newbranch}"
|
||
git checkout -q -b "$newbranch" "origin/${TARGET_BRANCH}"
|
||
elif [ "$fold" != "1" ]; then
|
||
die "close 在任务分支内须加 --fold(并入当前任务 PR);或到主检出运行(将建 feature/card-close-* 分支+PR)"
|
||
fi
|
||
local n=0 sha subject pr closed_list=""
|
||
while IFS='|' read -r c b m; do
|
||
[ -n "$c" ] || continue
|
||
if [ "$m" = "ARCHIVE" ]; then
|
||
mkdir -p .task/archive
|
||
git mv "$c" ".task/archive/${c##*/}"
|
||
closed_list="$closed_list ${c##*/}(already-complete)"
|
||
n=$((n+1))
|
||
continue
|
||
fi
|
||
sha="${m%%|*}"; subject="${m#*|}"
|
||
pr=$(printf '%s' "$subject" | sed -n 's/.*(#\([0-9]\+\)) from.*/\1/p')
|
||
[ -n "$pr" ] || pr=unknown
|
||
card_backfill "$c" "$pr" "$sha" "$subject" "${web_base}/${owner}/${repo}"
|
||
mkdir -p .task/archive
|
||
git mv "$c" ".task/archive/${c##*/}"
|
||
closed_list="$closed_list ${c##*/}(PR#${pr})"
|
||
n=$((n+1))
|
||
done <<EOF
|
||
$cards
|
||
EOF
|
||
local msg; msg=$(mktemp)
|
||
{ echo "Close ${n} merged task card(s) and archive them"
|
||
echo
|
||
echo "Why: branches merged into ${TARGET_BRANCH} had left their task cards open; backfill status and archive to prevent bookkeeping debt."
|
||
echo "Evidence: $(echo $closed_list) — merges verified via git log --merges against origin/${TARGET_BRANCH}."
|
||
echo "Impact: .task/*.md moved to .task/archive/ with status backfilled; no code or submodule change."
|
||
echo "Refs: task.sh close (card-autoclose-r1)"
|
||
} > "$msg"
|
||
git add .task
|
||
git commit -q -F "$msg"; rm -f "$msg"
|
||
# push 失败重试一次:家服隧道间歇抖动,单次尝试曾让钩子闭合半途而废(2026-09-30)。
|
||
git push -q -u origin HEAD || { sleep 5; git push -q -u origin HEAD; }
|
||
local headbr pr_url=""
|
||
headbr=$(git rev-parse --abbrev-ref HEAD)
|
||
if [ -n "${GITEA_TOKEN:-}" ]; then
|
||
# HTTPS 面偶发 broken pipe / 超时(AGENTS.md 访问事实节),单发 curl 会让 PR 静默
|
||
# 缺失(闭合分支推了、PR 没建,2026-09-30 三次踩中):退避重试 + 失败可见 + 终局
|
||
# 按 head 查回可能已存在的 PR(post-merge 钩子与手动 close 并发时 PR 会重复请求)。
|
||
local resp attempt api="${web_base}/api/v1/repos/${owner}/${repo}/pulls"
|
||
for attempt in 1 2 3 4; do
|
||
resp=$(curl -sS -m 25 -X POST -H "Authorization: token ${GITEA_TOKEN}" -H 'Content-Type: application/json' \
|
||
"$api" \
|
||
-d "{\"head\":\"${headbr}\",\"base\":\"${TARGET_BRANCH}\",\"title\":\"Close merged task cards (${headbr})\",\"body\":\"Auto-generated by task.sh close; see commit message for evidence.\"}" 2>&1) || resp=""
|
||
pr_url=$(printf '%s' "$resp" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("html_url","") if isinstance(d,dict) else "")' 2>/dev/null || true)
|
||
[ -n "$pr_url" ] && break
|
||
echo "[close] 建 PR 第 ${attempt} 次未成功${resp:+($(printf '%s' "$resp" | head -c 120))},退避重试" >&2
|
||
[ "$attempt" = 4 ] || sleep $((attempt * 3))
|
||
done
|
||
if [ -z "$pr_url" ]; then
|
||
resp=$(curl -sS -m 25 -H "Authorization: token ${GITEA_TOKEN}" "$api?state=open" 2>/dev/null || true)
|
||
pr_url=$(printf '%s' "$resp" | python3 -c 'import json,sys
|
||
for p in json.load(sys.stdin):
|
||
if p["head"]["ref"] == sys.argv[1]:
|
||
print(p["html_url"]); break' "$headbr" 2>/dev/null || true)
|
||
fi
|
||
fi
|
||
if [ -n "$newbranch" ]; then git checkout -q "${TARGET_BRANCH}"; fi
|
||
if [ -n "$pr_url" ]; then
|
||
echo "[pr] ${pr_url}(合入权在所有者)"
|
||
else
|
||
echo "[next] 建 PR:${web_base}/${owner}/${repo}/pulls/new?head=${headbr}(合入权在所有者)"
|
||
fi
|
||
echo "[closed] ${n} 张已合入卡片已回填归档,提交于 ${headbr} 并推送"
|
||
}
|
||
|
||
cmd_finish() {
|
||
need_repo
|
||
local abandon="${1:-}"
|
||
local branch head common mainrepo
|
||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||
head=$(git rev-parse HEAD)
|
||
common=$(git rev-parse --git-common-dir); common=$(cd "$common" && pwd)
|
||
mainrepo=$(dirname "$common")
|
||
# 防护(T0 事故教训):finish 必须在任务 worktree 内运行——本脚本把 $PWD 当
|
||
# worktree 回收(rm -rf);从主检出运行会误删主检出。mainrepo==PWD 即主检出。
|
||
[ "$(cd "$mainrepo" && pwd)" = "$PWD" ] && die "task-finish 须在任务 worktree 内运行(cd 到 worktree 再执行),防止误删主检出"
|
||
git fetch -q origin "${TARGET_BRANCH}" 2>/dev/null || true
|
||
if [ "$abandon" = "--abandon" ]; then
|
||
local reason="${2:-}"; [ -n "$reason" ] || die "--abandon 需要原因:task.sh finish --abandon <reason>"
|
||
local untracked; untracked=$(git status --porcelain | grep -c '^??' || true)
|
||
[ "$untracked" -gt 0 ] && die "存在 ${untracked} 个未跟踪文件 — 先处置或用 task-finish FORCE_ABANDON=1"
|
||
[ "${FORCE_ABANDON:-0}" = "1" ] || { echo "[confirm] 放弃未合入工作属破坏性动作:重跑 FORCE_ABANDON=1 task.sh finish --abandon \"${reason}\""; exit 5; }
|
||
echo "[abandoned] branch=${branch} reason=${reason} recorded_at=$(date -u +%FT%TZ)"
|
||
else
|
||
git merge-base --is-ancestor "$head" "origin/${TARGET_BRANCH}" || die "分支未合入 origin/${TARGET_BRANCH}——先走 PR 合入;放弃则用 --abandon <reason>"
|
||
echo "[verified] ${branch} 已包含于 origin/${TARGET_BRANCH};成果已落盘"
|
||
fi
|
||
# git 对含 gitlink 的 worktree 无条件拒绝 remove(查索引),回收走 rm -rf + prune。
|
||
# 此处不得跑 submodule deinit:submodule.<name>.url 写在共享的 .git/config 里,deinit
|
||
# 会把其他检出/worktree 的子模块一并标成未初始化(2026-09-30 实测 finish 后主检出 uninit=12)。
|
||
git -C "$mainrepo" worktree remove "$PWD" 2>/dev/null || { rm -rf "$PWD"; git -C "$mainrepo" worktree prune; }
|
||
git -C "$mainrepo" branch -d "$branch" 2>/dev/null || git -C "$mainrepo" branch -D "$branch"
|
||
git -C "$mainrepo" worktree prune
|
||
git push -q --delete origin "$branch" 2>/dev/null || true
|
||
echo "[recycled] worktree 与分支 ${branch} 已回收(本地+远端,$(date -u +%FT%TZ))"
|
||
# 合入成功→自动闭合任务卡(含本任务卡;经 feature/card-close-* 分支+PR,合入权在所有者)
|
||
( cd "$mainrepo" && bash scripts/task.sh close ) \
|
||
|| echo "[next] 卡片自动闭合未完成——在主检出运行 make task-close"
|
||
}
|
||
|
||
case "${1:-}" in
|
||
precheck) shift; cmd_precheck "$@" ;;
|
||
start) shift; cmd_start "$@" ;;
|
||
sync) shift; cmd_sync "$@" ;;
|
||
close) shift; cmd_close "$@" ;;
|
||
finish) shift; cmd_finish "$@" ;;
|
||
*) echo "usage: task.sh <precheck|start|sync|close|finish> [args]"; exit 2 ;;
|
||
esac
|