Why: git refuses to remove a worktree with initialized submodules; the first real dogfood run of task-finish hit this on the empty-list fix task. Evidence: the fix task's worktree recycled only after manual force-remove; deinit-then-remove path exercised on the same state afterwards. Impact: finish deinitializes submodules, removes, prunes, and deletes the remote branch; unmerged-result guard unchanged. Refs: .task/fix-empty-list-json.md
144 lines
7.4 KiB
Bash
Executable File
144 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 任务启动门禁:precheck / start / sync / 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_dirty
|
||
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_dirty=$(git submodule status --recursive 2>/dev/null | { grep -cE '^\+|^U' || 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_dirty=${sub_dirty}"
|
||
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 ' +')
|
||
echo "[worktrees] linked=${wt}"
|
||
[ -n "$merged" ] && echo "[recycle] merged un-recycled branches: $(echo $merged)"
|
||
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_dirty" -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/' | wc -l)" -eq 0 ] || die "dirty tree — 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
|
||
rmain=$(git rev-parse "origin/${TARGET_BRANCH}")
|
||
slug=$(echo "$name" | tr '/' '-')
|
||
wt="$(dirname "$(pwd)")/$(basename "$(pwd)")-wt-${slug}"
|
||
[ -e "$wt" ] && die "worktree path exists: $wt"
|
||
git worktree add -q "$wt" -b "$name" "$rmain"
|
||
(cd "$wt" && git submodule update --init --recursive >/dev/null 2>&1 || echo "[warn] submodule init incomplete in worktree")
|
||
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) |
|
||
|
||
## 结果与验证(完成后填写)
|
||
|
||
- 结果:
|
||
- 验证:
|
||
- 影响与剩余项:
|
||
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
|
||
base=$(git rev-parse "origin/${TARGET_BRANCH}"); head=$(git rev-parse HEAD)
|
||
card=$(ls .task/*.md 2>/dev/null | head -1 || true)
|
||
if [ -n "$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
|
||
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 等),证据写入任务卡"
|
||
}
|
||
|
||
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")
|
||
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 submodule deinit --all -f >/dev/null 2>&1 || true
|
||
git -C "$mainrepo" worktree remove "$PWD" 2>/dev/null || { echo "finish: worktree remove 失败(含未清理内容)——已验证合入,可手动 rm -rf 后 git worktree prune" >&2; exit 1; }
|
||
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))"
|
||
}
|
||
|
||
case "${1:-}" in
|
||
precheck) shift; cmd_precheck "$@" ;;
|
||
start) shift; cmd_start "$@" ;;
|
||
sync) shift; cmd_sync "$@" ;;
|
||
finish) shift; cmd_finish "$@" ;;
|
||
*) echo "usage: task.sh <precheck|start|sync|finish> [args]"; exit 2 ;;
|
||
esac
|