#!/usr/bin/env bash # Replay linear dev history as feature/fix branches with merge --no-ff. # Requires: scripts/history-epic-manifest.tsv and tag backup/pre-rewrite-2026-09-13 set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" MANIFEST="${ROOT_DIR}/scripts/history-epic-manifest.tsv" BACKUP_TAG="backup/pre-rewrite-2026-09-13" SOURCE_BRANCH="dev" TARGET_BRANCH="dev-rewritten" DRY_RUN="${DRY_RUN:-0}" declare -A MSG_OVERRIDE=( [d27b0ad]="docs(ops): 建立仓库基础架构、三分支规范与环境运行文档" [91985a2]="fix(quality): 闭环 Go 质量门禁(鉴权富化/V0.5/vet/health/KB gate/记忆轮询)" [546de52]="ci(qa): add QA-001 gate with go vet, short tests, and migration check." [e82b624]="docs(plan): add production governance plan and Phase 0 authz matrix" [77d3da1]="docs(plan): add Phase 1 IAM and authorization implementation plan" [ba249a5]="chore(scripts): fix backfill script import and run projection for ws_intake" ) log() { echo "[rewrite] $*"; } die() { echo "[rewrite] ERROR: $*" >&2; exit 1; } [[ -f "${MANIFEST}" ]] || die "missing manifest: ${MANIFEST}" git rev-parse "${BACKUP_TAG}" >/dev/null 2>&1 || die "missing backup tag ${BACKUP_TAG}" cd "${ROOT_DIR}" if [[ "${DRY_RUN}" == "1" ]]; then log "DRY_RUN=1 — manifest entries:" tail -n +2 "${MANIFEST}" exit 0 fi current_branch="$(git branch --show-current)" [[ "${current_branch}" == "${SOURCE_BRANCH}" ]] || die "checkout ${SOURCE_BRANCH} first (on ${current_branch})" if git show-ref --verify --quiet "refs/heads/${TARGET_BRANCH}"; then die "branch ${TARGET_BRANCH} already exists; delete it before rerun" fi log "creating orphan branch ${TARGET_BRANCH}" git checkout --orphan "${TARGET_BRANCH}" git rm -rf . >/dev/null 2>&1 || true git commit --allow-empty -m "chore: history rewrite root anchor" commit_range() { local start="$1" end="$2" if [[ "${start}" == "${end}" ]]; then echo "${start}" return fi if git rev-parse "${start}^" >/dev/null 2>&1; then git rev-list --reverse "${start}^..${end}" else echo "${start}" git rev-list --reverse "${start}..${end}" fi } cherry_pick_commit() { local hash="$1" if ! git cherry-pick "${hash}"; then die "cherry-pick failed at ${hash}; run git cherry-pick --abort and fix manifest/script" fi if [[ -n "${MSG_OVERRIDE[${hash}]+x}" ]]; then git commit --amend -m "${MSG_OVERRIDE[${hash}]}" fi } cherry_pick_range() { local start="$1" end="$2" local hash while read -r hash; do [[ -n "${hash}" ]] || continue log " cherry-pick ${hash}" cherry_pick_commit "${hash}" done < <(commit_range "${start}" "${end}") } while IFS=$'\t' read -r slug branch_type start end merge_msg; do branch="${branch_type}/${slug}" log "epic ${slug}: ${branch} (${start}..${end})" git checkout -b "${branch}" "${TARGET_BRANCH}" cherry_pick_range "${start}" "${end}" git checkout "${TARGET_BRANCH}" git merge --no-ff "${branch}" -m "${merge_msg}" git branch -d "${branch}" done < <(tail -n +2 "${MANIFEST}") log "verifying tree matches ${BACKUP_TAG}" if ! git diff --quiet "${BACKUP_TAG}" HEAD; then die "tree mismatch after rewrite — aborting before branch swap" fi log "replacing ${SOURCE_BRANCH} with rewritten history" git branch -m "${SOURCE_BRANCH}" "${SOURCE_BRANCH}-old-linear" git branch -m "${TARGET_BRANCH}" "${SOURCE_BRANCH}" log "done. old linear history preserved on ${SOURCE_BRANCH}-old-linear" log "verify: git diff ${BACKUP_TAG} ${SOURCE_BRANCH} (should be empty)"