diff --git a/scripts/migrate-workdir-layout-20260724.sh b/scripts/migrate-workdir-layout-20260724.sh new file mode 100755 index 00000000..ad0ddfca --- /dev/null +++ b/scripts/migrate-workdir-layout-20260724.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: + scripts/migrate-workdir-layout-20260724.sh --workdir-target [--apply] + +Migrates old runtime workdir materialization layout: + + /working-directories//materialization.json + /working-directories//root// + +to the new layout: + + //materialization.json + //checkout/ + +The script defaults to dry-run. It mutates files only when --apply is passed. +Run this while the runtime/server and workers using the target are stopped. + +Options: + --workdir-target Runtime workdir materialization target. + --apply Perform migration. Without this, only prints actions. + -h, --help Show this help. + +Requirements: + bash, git, jq, mktemp +USAGE +} + +log() { + printf '%s\n' "$*" +} + +fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1" +} + +json_string() { + local file=$1 + local expr=$2 + jq -er "$expr | strings" "$file" +} + +apply=0 +workdir_target="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --workdir-target) + [[ $# -ge 2 ]] || fail "--workdir-target requires a path" + workdir_target=$2 + shift 2 + ;; + --apply) + apply=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac +done + +[[ -n "$workdir_target" ]] || fail "--workdir-target is required" +need_cmd git +need_cmd jq +need_cmd mktemp + +[[ -d "$workdir_target" ]] || fail "workdir target does not exist or is not a directory: $workdir_target" +old_root=$workdir_target/working-directories +if [[ ! -d "$old_root" ]]; then + log "no old workdir layout found: $old_root" + exit 0 +fi + +if [[ $apply -eq 0 ]]; then + log "dry-run: no files will be changed; pass --apply to migrate" +else + log "apply: migrating old workdir layout under $workdir_target" +fi + +shopt -s nullglob +records=("$old_root"/*/materialization.json) +shopt -u nullglob + +if [[ ${#records[@]} -eq 0 ]]; then + log "no materialization records found under $old_root" + exit 0 +fi + +migrated=0 + +for record in "${records[@]}"; do + old_dir=$(dirname "$record") + workdir_id=$(basename "$old_dir") + new_dir=$workdir_target/$workdir_id + new_record=$new_dir/materialization.json + new_checkout=$new_dir/checkout + + old_checkout=$(json_string "$record" '.root') + source_repository_path=$(json_string "$record" '.source_repository_path') + + [[ "$old_checkout" = "$old_dir"/root/* ]] || fail "$record root is not under expected old root/ layout: $old_checkout" + [[ -d "$old_checkout" ]] || fail "$record root does not exist or is not a directory: $old_checkout" + [[ -d "$source_repository_path" ]] || fail "$record source_repository_path does not exist or is not a directory: $source_repository_path" + + root_parent=$(dirname "$old_checkout") + [[ "$root_parent" = "$old_dir/root" ]] || fail "$record root parent is not $old_dir/root: $root_parent" + + shopt -s nullglob dotglob + root_children=("$old_dir/root"/*) + shopt -u nullglob dotglob + [[ ${#root_children[@]} -eq 1 ]] || fail "$old_dir/root must contain exactly one checkout; found ${#root_children[@]}" + [[ "${root_children[0]}" = "$old_checkout" ]] || fail "$record root does not match the only checkout under $old_dir/root" + + if [[ -e "$new_dir" && "$new_dir" != "$old_dir" ]]; then + fail "target workdir directory already exists: $new_dir" + fi + if [[ -e "$new_checkout" ]]; then + fail "target checkout already exists: $new_checkout" + fi + if [[ -e "$new_record" ]]; then + fail "target materialization record already exists: $new_record" + fi + + log "workdir $workdir_id" + log " move checkout: $old_checkout -> $new_checkout" + log " rewrite record: $record -> $new_record" + log " repair git worktree metadata in: $source_repository_path" + + if [[ $apply -eq 1 ]]; then + mkdir -p "$new_dir" + mv "$old_checkout" "$new_checkout" + + tmp_record=$(mktemp "$new_dir/materialization.json.tmp.XXXXXX") + jq --arg root "$new_checkout" '.root = $root' "$record" > "$tmp_record" + mv "$tmp_record" "$new_record" + + git -C "$source_repository_path" worktree repair "$new_checkout" + + rm "$record" + rmdir "$old_dir/root" + rmdir "$old_dir" + fi + migrated=$((migrated + 1)) +done + +if [[ $apply -eq 1 ]]; then + rmdir "$old_root" 2>/dev/null || true + log "migrated $migrated workdir(s)" +else + log "dry-run complete: $migrated workdir(s) would be migrated" +fi