#!/usr/bin/env bash # archeflow-review.sh — Get a git diff for Guardian review, with stats. # # Standalone diff helper for af-review. No PDCA orchestration — just extracts # the right diff and reports stats so the Claude Code agent can feed it to # Guardian (or other reviewers). # # Usage: # archeflow-review.sh # Uncommitted changes (staged + unstaged) # archeflow-review.sh --branch feat/batch-api # Branch diff vs main # archeflow-review.sh --commit HEAD~3..HEAD # Commit range # archeflow-review.sh --base develop # Override base branch (default: main) # archeflow-review.sh --stat-only # Only print stats, no diff output # # Output: # Prints the diff to stdout. Stats go to stderr so they don't pollute the diff. # Exit code 0 if diff is non-empty, 1 if empty (nothing to review). set -euo pipefail # --------------------------------------------------------------------------- # Globals # --------------------------------------------------------------------------- BASE_BRANCH="main" MODE="uncommitted" # uncommitted | branch | commit TARGET="" STAT_ONLY="false" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- die() { echo "[af-review] ERROR: $*" >&2 exit 1 } info() { echo "[af-review] $*" >&2 } # Print diff stats (files changed, insertions, deletions) to stderr. print_stats() { local diff_text="$1" local files_changed lines_added lines_removed total_lines files_changed=$(echo "$diff_text" | grep -c '^diff --git' || true) lines_added=$(echo "$diff_text" | grep -c '^+[^+]' || true) lines_removed=$(echo "$diff_text" | grep -c '^-[^-]' || true) total_lines=$(echo "$diff_text" | wc -l | tr -d ' ') info "--- Review Stats ---" info "Files changed: ${files_changed}" info "Lines added: +${lines_added}" info "Lines removed: -${lines_removed}" info "Diff size: ${total_lines} lines" if [[ "$total_lines" -gt 500 ]]; then info "Warning: large diff (>500 lines). Consider reviewing per-file." fi } # Detect the default base branch (main or master). detect_base_branch() { if git show-ref --verify --quiet "refs/heads/main" 2>/dev/null; then echo "main" elif git show-ref --verify --quiet "refs/heads/master" 2>/dev/null; then echo "master" else echo "main" fi } # --------------------------------------------------------------------------- # Argument parsing # --------------------------------------------------------------------------- parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --branch) MODE="branch" TARGET="${2:?Missing branch name after --branch}" shift 2 ;; --commit) MODE="commit" TARGET="${2:?Missing commit range after --commit}" shift 2 ;; --base) BASE_BRANCH="${2:?Missing base branch after --base}" shift 2 ;; --stat-only) STAT_ONLY="true" shift ;; -h|--help) echo "Usage: $0 [--branch ] [--commit ] [--base ] [--stat-only]" echo "" echo " (no args) Review uncommitted changes (staged + unstaged)" echo " --branch Review branch diff against base (default: main)" echo " --commit Review a commit range (e.g. HEAD~3..HEAD)" echo " --base Override base branch (default: auto-detect main/master)" echo " --stat-only Print stats only, no diff output" exit 0 ;; *) die "Unknown argument: $1. Use --help for usage." ;; esac done } # --------------------------------------------------------------------------- # Diff extraction # --------------------------------------------------------------------------- get_diff() { local diff_text="" case "$MODE" in uncommitted) # Combine staged and unstaged changes against HEAD diff_text=$(git diff HEAD 2>/dev/null || true) if [[ -z "$diff_text" ]]; then # Maybe everything is staged, try just staged diff_text=$(git diff --cached 2>/dev/null || true) fi ;; branch) # Verify target branch exists if ! git show-ref --verify --quiet "refs/heads/${TARGET}" 2>/dev/null; then # Maybe it's a remote branch if ! git rev-parse --verify "${TARGET}" &>/dev/null; then die "Branch '${TARGET}' not found." fi fi diff_text=$(git diff "${BASE_BRANCH}...${TARGET}" 2>/dev/null || true) ;; commit) # Validate commit range resolves if ! git rev-parse "${TARGET}" &>/dev/null 2>&1; then die "Invalid commit range: '${TARGET}'" fi diff_text=$(git diff "${TARGET}" 2>/dev/null || true) ;; esac echo "$diff_text" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { # Verify we're in a git repo if ! git rev-parse --is-inside-work-tree &>/dev/null; then die "Not inside a git repository." fi parse_args "$@" # Auto-detect base branch if not overridden if [[ "$BASE_BRANCH" == "main" ]]; then BASE_BRANCH=$(detect_base_branch) fi # Describe what we're reviewing case "$MODE" in uncommitted) info "Reviewing: uncommitted changes vs HEAD" ;; branch) info "Reviewing: branch '${TARGET}' vs '${BASE_BRANCH}'" ;; commit) info "Reviewing: commit range '${TARGET}'" ;; esac local diff_text diff_text=$(get_diff) # Validate non-empty if [[ -z "$diff_text" ]]; then info "No changes found. Nothing to review." exit 1 fi # Print stats to stderr print_stats "$diff_text" # Output the diff to stdout (unless stat-only) if [[ "$STAT_ONLY" != "true" ]]; then echo "$diff_text" fi } main "$@"