feat: add archeflow-git.sh for per-phase commits and rollback
This commit is contained in:
603
lib/archeflow-git.sh
Executable file
603
lib/archeflow-git.sh
Executable file
@@ -0,0 +1,603 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# archeflow-git.sh — Git-per-phase commit strategy for ArcheFlow runs.
|
||||||
|
#
|
||||||
|
# Creates a branch per run, commits after each phase/agent, merges on success,
|
||||||
|
# and supports rollback to any phase boundary.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# archeflow-git.sh init <run_id> # Create branch, switch to it
|
||||||
|
# archeflow-git.sh commit <run_id> <phase> <msg> [files...] # Stage + commit
|
||||||
|
# archeflow-git.sh phase-commit <run_id> <phase> # Commit all phase artifacts
|
||||||
|
# archeflow-git.sh merge <run_id> [--squash|--no-ff] # Merge to base branch
|
||||||
|
# archeflow-git.sh rollback <run_id> --to <phase> # Reset to end of phase
|
||||||
|
# archeflow-git.sh status <run_id> # Show branch status
|
||||||
|
# archeflow-git.sh cleanup <run_id> # Delete branch after merge
|
||||||
|
#
|
||||||
|
# Configuration is read from .archeflow/config.yaml if it exists.
|
||||||
|
# All operations respect ArcheFlow safety rules: no force-push, no main modification.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Globals
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ARCHEFLOW_DIR=".archeflow"
|
||||||
|
CONFIG_FILE="${ARCHEFLOW_DIR}/config.yaml"
|
||||||
|
|
||||||
|
# Defaults (overridden by config if present)
|
||||||
|
BRANCH_PREFIX="archeflow/"
|
||||||
|
COMMIT_STYLE="conventional" # conventional | simple
|
||||||
|
MERGE_STRATEGY="squash" # squash | no-ff | rebase
|
||||||
|
AUTO_PUSH="false"
|
||||||
|
SIGNING_KEY=""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "[archeflow-git] ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo "[archeflow-git] $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read a yaml key (simple single-level, no dependencies beyond grep/sed).
|
||||||
|
# Falls back to default if key not found or file missing.
|
||||||
|
yaml_get() {
|
||||||
|
local file="$1" key="$2" default="${3:-}"
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
local val
|
||||||
|
val=$(grep -E "^\s*${key}:" "$file" 2>/dev/null | head -1 | sed 's/^[^:]*:\s*//' | sed 's/\s*#.*//' | sed 's/^"\(.*\)"$/\1/' | sed "s/^'\(.*\)'$/\1/")
|
||||||
|
if [[ -n "$val" && "$val" != "null" ]]; then
|
||||||
|
echo "$val"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "$default"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_config() {
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
BRANCH_PREFIX=$(yaml_get "$CONFIG_FILE" "branch_prefix" "$BRANCH_PREFIX")
|
||||||
|
COMMIT_STYLE=$(yaml_get "$CONFIG_FILE" "commit_style" "$COMMIT_STYLE")
|
||||||
|
MERGE_STRATEGY=$(yaml_get "$CONFIG_FILE" "merge_strategy" "$MERGE_STRATEGY")
|
||||||
|
AUTO_PUSH=$(yaml_get "$CONFIG_FILE" "auto_push" "$AUTO_PUSH")
|
||||||
|
SIGNING_KEY=$(yaml_get "$CONFIG_FILE" "signing_key" "$SIGNING_KEY")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
branch_name() {
|
||||||
|
local run_id="$1"
|
||||||
|
echo "${BRANCH_PREFIX}${run_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the base branch (the branch we were on before creating the run branch).
|
||||||
|
# Stored in .archeflow/runs/<run_id>/base-branch during init.
|
||||||
|
get_base_branch() {
|
||||||
|
local run_id="$1"
|
||||||
|
local base_file="${ARCHEFLOW_DIR}/runs/${run_id}/base-branch"
|
||||||
|
if [[ -f "$base_file" ]]; then
|
||||||
|
cat "$base_file"
|
||||||
|
else
|
||||||
|
echo "main"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build commit signing args if signing_key is configured.
|
||||||
|
signing_args() {
|
||||||
|
if [[ -n "$SIGNING_KEY" ]]; then
|
||||||
|
echo "-c user.signingkey=${SIGNING_KEY} -c gpg.format=ssh -c commit.gpgsign=true"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify we are on the expected branch.
|
||||||
|
assert_on_branch() {
|
||||||
|
local expected="$1"
|
||||||
|
local current
|
||||||
|
current=$(git branch --show-current 2>/dev/null || true)
|
||||||
|
if [[ "$current" != "$expected" ]]; then
|
||||||
|
die "Expected to be on branch '${expected}', but on '${current}'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for uncommitted changes.
|
||||||
|
has_uncommitted_changes() {
|
||||||
|
! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format the commit message based on style.
|
||||||
|
format_message() {
|
||||||
|
local phase="$1" msg="$2"
|
||||||
|
if [[ "$COMMIT_STYLE" == "simple" ]]; then
|
||||||
|
echo "${phase}: ${msg}"
|
||||||
|
else
|
||||||
|
echo "archeflow(${phase}): ${msg}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Push if auto_push is enabled.
|
||||||
|
maybe_push() {
|
||||||
|
local branch="$1"
|
||||||
|
if [[ "$AUTO_PUSH" == "true" ]]; then
|
||||||
|
info "Pushing ${branch} to remote..."
|
||||||
|
git push origin "$branch" 2>/dev/null || info "Push failed (non-fatal, remote may not exist)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Commands
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
cmd_init() {
|
||||||
|
local run_id="$1"
|
||||||
|
local branch
|
||||||
|
branch=$(branch_name "$run_id")
|
||||||
|
|
||||||
|
# Record the current branch as the base branch
|
||||||
|
local current_branch
|
||||||
|
current_branch=$(git branch --show-current 2>/dev/null || echo "main")
|
||||||
|
|
||||||
|
# Check for existing branch
|
||||||
|
if git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
|
||||||
|
die "Branch '${branch}' already exists. Use a different run_id or clean up first."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stash if dirty
|
||||||
|
if has_uncommitted_changes; then
|
||||||
|
info "Stashing uncommitted changes..."
|
||||||
|
git stash push -m "archeflow-git: auto-stash before run ${run_id}" --quiet
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create and switch to the run branch
|
||||||
|
git checkout -b "$branch" --quiet
|
||||||
|
info "Created and switched to branch: ${branch}"
|
||||||
|
|
||||||
|
# Store base branch for later merge
|
||||||
|
mkdir -p "${ARCHEFLOW_DIR}/runs/${run_id}"
|
||||||
|
echo "$current_branch" > "${ARCHEFLOW_DIR}/runs/${run_id}/base-branch"
|
||||||
|
|
||||||
|
maybe_push "$branch"
|
||||||
|
info "Init complete for run: ${run_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_commit() {
|
||||||
|
local run_id="$1"
|
||||||
|
local phase="$2"
|
||||||
|
local msg="$3"
|
||||||
|
shift 3
|
||||||
|
local extra_files=("$@")
|
||||||
|
|
||||||
|
local branch
|
||||||
|
branch=$(branch_name "$run_id")
|
||||||
|
assert_on_branch "$branch"
|
||||||
|
|
||||||
|
# Stage artifact directory for this run
|
||||||
|
local artifact_dir="${ARCHEFLOW_DIR}/artifacts/${run_id}"
|
||||||
|
if [[ -d "$artifact_dir" ]]; then
|
||||||
|
git add "$artifact_dir" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage the event log
|
||||||
|
local event_file="${ARCHEFLOW_DIR}/events/${run_id}.jsonl"
|
||||||
|
if [[ -f "$event_file" ]]; then
|
||||||
|
git add "$event_file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage the run metadata (base-branch file etc.)
|
||||||
|
local run_meta="${ARCHEFLOW_DIR}/runs/${run_id}"
|
||||||
|
if [[ -d "$run_meta" ]]; then
|
||||||
|
git add "$run_meta" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage any extra files passed as arguments
|
||||||
|
for f in "${extra_files[@]}"; do
|
||||||
|
if [[ -e "$f" ]]; then
|
||||||
|
git add "$f" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
info "Warning: file '${f}' does not exist, skipping"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if there is anything to commit
|
||||||
|
if git diff --cached --quiet 2>/dev/null; then
|
||||||
|
info "Nothing to commit for ${phase}: ${msg}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local commit_msg
|
||||||
|
commit_msg=$(format_message "$phase" "$msg")
|
||||||
|
|
||||||
|
# Build signing args
|
||||||
|
local sign_args
|
||||||
|
sign_args=$(signing_args)
|
||||||
|
|
||||||
|
if [[ -n "$sign_args" ]]; then
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
git $sign_args commit -m "$commit_msg" --quiet
|
||||||
|
else
|
||||||
|
git commit -m "$commit_msg" --quiet
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Committed: ${commit_msg}"
|
||||||
|
maybe_push "$branch"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_phase_commit() {
|
||||||
|
local run_id="$1"
|
||||||
|
local phase="$2"
|
||||||
|
|
||||||
|
local branch
|
||||||
|
branch=$(branch_name "$run_id")
|
||||||
|
assert_on_branch "$branch"
|
||||||
|
|
||||||
|
local artifact_dir="${ARCHEFLOW_DIR}/artifacts/${run_id}"
|
||||||
|
|
||||||
|
# Determine the next phase for the transition message
|
||||||
|
local next_phase=""
|
||||||
|
case "$phase" in
|
||||||
|
plan) next_phase="do" ;;
|
||||||
|
do) next_phase="check" ;;
|
||||||
|
check) next_phase="act" ;;
|
||||||
|
act) next_phase="complete" ;;
|
||||||
|
*) next_phase="next" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Stage all artifacts matching the phase prefix
|
||||||
|
if [[ -d "$artifact_dir" ]]; then
|
||||||
|
for f in "${artifact_dir}/${phase}-"*; do
|
||||||
|
[[ -e "$f" ]] && git add "$f" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage event log
|
||||||
|
local event_file="${ARCHEFLOW_DIR}/events/${run_id}.jsonl"
|
||||||
|
if [[ -f "$event_file" ]]; then
|
||||||
|
git add "$event_file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if there is anything to commit
|
||||||
|
if git diff --cached --quiet 2>/dev/null; then
|
||||||
|
info "Nothing to commit for phase transition: ${phase}→${next_phase}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local commit_msg
|
||||||
|
commit_msg=$(format_message "${phase}→${next_phase}" "phase transition")
|
||||||
|
|
||||||
|
local sign_args
|
||||||
|
sign_args=$(signing_args)
|
||||||
|
|
||||||
|
if [[ -n "$sign_args" ]]; then
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
git $sign_args commit -m "$commit_msg" --quiet
|
||||||
|
else
|
||||||
|
git commit -m "$commit_msg" --quiet
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Committed phase transition: ${phase} → ${next_phase}"
|
||||||
|
maybe_push "$branch"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_merge() {
|
||||||
|
local run_id="$1"
|
||||||
|
local strategy="${2:---squash}"
|
||||||
|
|
||||||
|
# Strip leading -- if present for comparison
|
||||||
|
strategy="${strategy#--}"
|
||||||
|
|
||||||
|
# Validate strategy
|
||||||
|
case "$strategy" in
|
||||||
|
squash|no-ff|rebase) ;;
|
||||||
|
*) die "Unknown merge strategy: ${strategy}. Use --squash, --no-ff, or --rebase." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local branch
|
||||||
|
branch=$(branch_name "$run_id")
|
||||||
|
|
||||||
|
# Verify we are on the run branch
|
||||||
|
assert_on_branch "$branch"
|
||||||
|
|
||||||
|
# Verify no uncommitted changes
|
||||||
|
if has_uncommitted_changes; then
|
||||||
|
die "Uncommitted changes on branch '${branch}'. Commit or stash before merging."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local base_branch
|
||||||
|
base_branch=$(get_base_branch "$run_id")
|
||||||
|
|
||||||
|
# Switch to base branch
|
||||||
|
git checkout "$base_branch" --quiet
|
||||||
|
info "Switched to base branch: ${base_branch}"
|
||||||
|
|
||||||
|
case "$strategy" in
|
||||||
|
squash)
|
||||||
|
git merge --squash "$branch" --quiet
|
||||||
|
# Check if there are changes to commit (squash stages but doesn't commit)
|
||||||
|
if ! git diff --cached --quiet 2>/dev/null; then
|
||||||
|
local sign_args
|
||||||
|
sign_args=$(signing_args)
|
||||||
|
local commit_msg="feat: archeflow run ${run_id} complete"
|
||||||
|
if [[ -n "$sign_args" ]]; then
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
git $sign_args commit -m "$commit_msg" --quiet
|
||||||
|
else
|
||||||
|
git commit -m "$commit_msg" --quiet
|
||||||
|
fi
|
||||||
|
info "Squash-merged ${branch} into ${base_branch}"
|
||||||
|
else
|
||||||
|
info "No changes to merge (branch identical to base)"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
no-ff)
|
||||||
|
local sign_args
|
||||||
|
sign_args=$(signing_args)
|
||||||
|
if [[ -n "$sign_args" ]]; then
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
git $sign_args merge --no-ff "$branch" -m "feat: archeflow run ${run_id} complete" --quiet
|
||||||
|
else
|
||||||
|
git merge --no-ff "$branch" -m "feat: archeflow run ${run_id} complete" --quiet
|
||||||
|
fi
|
||||||
|
info "Merged ${branch} into ${base_branch} (no-ff)"
|
||||||
|
;;
|
||||||
|
rebase)
|
||||||
|
git rebase "$branch" --quiet
|
||||||
|
info "Rebased ${base_branch} onto ${branch}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
info "Merge complete. Branch '${branch}' preserved for inspection."
|
||||||
|
info "Run 'archeflow-git.sh cleanup ${run_id}' to delete the branch."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_rollback() {
|
||||||
|
local run_id="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
local target_phase=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--to) target_phase="$2"; shift 2 ;;
|
||||||
|
*) die "Unknown option: $1. Usage: rollback <run_id> --to <phase>" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$target_phase" ]]; then
|
||||||
|
die "Missing --to <phase>. Usage: rollback <run_id> --to <phase>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local branch
|
||||||
|
branch=$(branch_name "$run_id")
|
||||||
|
assert_on_branch "$branch"
|
||||||
|
|
||||||
|
# Find the target commit by searching commit messages.
|
||||||
|
# For phase targets like "plan", find the last commit containing that phase.
|
||||||
|
# For cycle targets like "cycle-2", find the cycle boundary commit.
|
||||||
|
local search_pattern
|
||||||
|
case "$target_phase" in
|
||||||
|
cycle-*)
|
||||||
|
local cycle_num="${target_phase#cycle-}"
|
||||||
|
search_pattern="cycle ${cycle_num}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
search_pattern="archeflow(${target_phase}"
|
||||||
|
if [[ "$COMMIT_STYLE" == "simple" ]]; then
|
||||||
|
search_pattern="${target_phase}:"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local target_commit
|
||||||
|
target_commit=$(git log --oneline --format="%H %s" "$branch" | grep -F "$search_pattern" | head -1 | awk '{print $1}')
|
||||||
|
|
||||||
|
if [[ -z "$target_commit" ]]; then
|
||||||
|
die "No commit found for phase '${target_phase}' on branch '${branch}'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local target_short
|
||||||
|
target_short=$(git log --oneline -1 "$target_commit")
|
||||||
|
|
||||||
|
# Show what will be lost
|
||||||
|
local commits_after
|
||||||
|
commits_after=$(git log --oneline "${target_commit}..HEAD")
|
||||||
|
|
||||||
|
if [[ -z "$commits_after" ]]; then
|
||||||
|
info "Already at the target commit. Nothing to roll back."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Rolling back to: ${target_short}"
|
||||||
|
echo ""
|
||||||
|
echo "The following commits will be removed:"
|
||||||
|
echo "$commits_after" | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
echo "This operation is destructive on the run branch."
|
||||||
|
echo "Type 'yes' to confirm:"
|
||||||
|
read -r confirm
|
||||||
|
|
||||||
|
if [[ "$confirm" != "yes" ]]; then
|
||||||
|
info "Rollback cancelled."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Perform the reset
|
||||||
|
git reset --hard "$target_commit" --quiet
|
||||||
|
info "Reset to: ${target_short}"
|
||||||
|
|
||||||
|
# Trim the events JSONL to match the rollback point.
|
||||||
|
# Find the commit timestamp and remove events after it.
|
||||||
|
local event_file="${ARCHEFLOW_DIR}/events/${run_id}.jsonl"
|
||||||
|
if [[ -f "$event_file" ]]; then
|
||||||
|
local commit_ts
|
||||||
|
commit_ts=$(git log -1 --format="%aI" "$target_commit")
|
||||||
|
# Keep only events with timestamps <= commit timestamp.
|
||||||
|
# Use jq to filter if available, otherwise leave the file as-is.
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
local tmp_file="${event_file}.tmp"
|
||||||
|
jq -c "select(.ts <= \"${commit_ts}\")" "$event_file" > "$tmp_file" 2>/dev/null || true
|
||||||
|
if [[ -s "$tmp_file" ]]; then
|
||||||
|
mv "$tmp_file" "$event_file"
|
||||||
|
git add "$event_file"
|
||||||
|
local sign_args
|
||||||
|
sign_args=$(signing_args)
|
||||||
|
local commit_msg
|
||||||
|
commit_msg=$(format_message "rollback" "to ${target_phase}")
|
||||||
|
if [[ -n "$sign_args" ]]; then
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
git $sign_args commit -m "$commit_msg" --quiet 2>/dev/null || true
|
||||||
|
else
|
||||||
|
git commit -m "$commit_msg" --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
info "Trimmed events JSONL to match rollback point"
|
||||||
|
else
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
info "Warning: could not trim events JSONL (file may need manual cleanup)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "Warning: jq not available, events JSONL not trimmed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Rollback complete. You are now at the end of the '${target_phase}' phase."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
local run_id="$1"
|
||||||
|
local branch
|
||||||
|
branch=$(branch_name "$run_id")
|
||||||
|
|
||||||
|
# Check if branch exists
|
||||||
|
if ! git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
|
||||||
|
die "Branch '${branch}' does not exist."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local base_branch
|
||||||
|
base_branch=$(get_base_branch "$run_id")
|
||||||
|
|
||||||
|
local ahead
|
||||||
|
ahead=$(git rev-list --count "${base_branch}..${branch}" 2>/dev/null || echo "?")
|
||||||
|
|
||||||
|
echo "Branch: ${branch}"
|
||||||
|
echo "Base: ${base_branch} (${ahead} commits ahead)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Commits:"
|
||||||
|
git log --oneline "${base_branch}..${branch}" 2>/dev/null | sed 's/^/ /' || echo " (none)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Determine current phase from latest commit message
|
||||||
|
local latest_msg
|
||||||
|
latest_msg=$(git log -1 --format="%s" "$branch" 2>/dev/null || echo "")
|
||||||
|
local current_phase="unknown"
|
||||||
|
local re_conv='archeflow\(([^)]+)\)'
|
||||||
|
local re_simple='^([a-z]+):'
|
||||||
|
if [[ "$latest_msg" =~ $re_conv ]]; then
|
||||||
|
current_phase="${BASH_REMATCH[1]}"
|
||||||
|
elif [[ "$latest_msg" =~ $re_simple ]]; then
|
||||||
|
current_phase="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
echo "Current phase: ${current_phase}"
|
||||||
|
|
||||||
|
# Count files changed
|
||||||
|
local files_changed
|
||||||
|
files_changed=$(git diff --name-only "${base_branch}...${branch}" 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
echo "Files changed (total): ${files_changed}"
|
||||||
|
|
||||||
|
# Check for uncommitted changes
|
||||||
|
local current
|
||||||
|
current=$(git branch --show-current 2>/dev/null || true)
|
||||||
|
if [[ "$current" == "$branch" ]]; then
|
||||||
|
if has_uncommitted_changes; then
|
||||||
|
echo "Uncommitted changes: YES"
|
||||||
|
else
|
||||||
|
echo "Uncommitted changes: none"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Uncommitted changes: (not on branch, cannot check)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_cleanup() {
|
||||||
|
local run_id="$1"
|
||||||
|
local branch
|
||||||
|
branch=$(branch_name "$run_id")
|
||||||
|
|
||||||
|
# Safety: don't delete if we're on the branch
|
||||||
|
local current
|
||||||
|
current=$(git branch --show-current 2>/dev/null || true)
|
||||||
|
if [[ "$current" == "$branch" ]]; then
|
||||||
|
die "Cannot delete branch '${branch}' while on it. Switch to another branch first."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if branch exists
|
||||||
|
if ! git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
|
||||||
|
die "Branch '${branch}' does not exist."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if branch is fully merged
|
||||||
|
local base_branch
|
||||||
|
base_branch=$(get_base_branch "$run_id")
|
||||||
|
if ! git merge-base --is-ancestor "$branch" "$base_branch" 2>/dev/null; then
|
||||||
|
echo "Warning: Branch '${branch}' is not fully merged into '${base_branch}'."
|
||||||
|
echo "Deleting it will lose unmerged commits."
|
||||||
|
echo "Type 'yes' to confirm:"
|
||||||
|
read -r confirm
|
||||||
|
if [[ "$confirm" != "yes" ]]; then
|
||||||
|
info "Cleanup cancelled."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
git branch -D "$branch" --quiet
|
||||||
|
else
|
||||||
|
git branch -d "$branch" --quiet
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up run metadata
|
||||||
|
rm -rf "${ARCHEFLOW_DIR}/runs/${run_id}"
|
||||||
|
|
||||||
|
info "Deleted branch: ${branch}"
|
||||||
|
info "Cleaned up run metadata for: ${run_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Usage: $0 <command> <run_id> [args...]" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Commands:" >&2
|
||||||
|
echo " init <run_id> Create branch and switch to it" >&2
|
||||||
|
echo " commit <run_id> <phase> <msg> [files] Stage relevant files and commit" >&2
|
||||||
|
echo " phase-commit <run_id> <phase> Commit all phase artifacts" >&2
|
||||||
|
echo " merge <run_id> [--squash|--no-ff] Merge run branch to base" >&2
|
||||||
|
echo " rollback <run_id> --to <phase> Reset to end of phase" >&2
|
||||||
|
echo " status <run_id> Show branch status and commits" >&2
|
||||||
|
echo " cleanup <run_id> Delete branch after merge" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cmd="$1"
|
||||||
|
local run_id="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
load_config
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
init) cmd_init "$run_id" ;;
|
||||||
|
commit) cmd_commit "$run_id" "$@" ;;
|
||||||
|
phase-commit) cmd_phase_commit "$run_id" "$@" ;;
|
||||||
|
merge) cmd_merge "$run_id" "$@" ;;
|
||||||
|
rollback) cmd_rollback "$run_id" "$@" ;;
|
||||||
|
status) cmd_status "$run_id" ;;
|
||||||
|
cleanup) cmd_cleanup "$run_id" ;;
|
||||||
|
*) die "Unknown command: ${cmd}" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user