#!/bin/bash # ClaudeTools Gitea Operations — bulletproof git/gitea helpers for the fleet # Extracted patterns from sync.sh into reusable commands set -e # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # --- Helper functions (extracted from sync.sh) --- get_repo_root() { local IDENTITY_PATH="" for candidate in "$HOME/.claude/identity.json" ".claude/identity.json"; do if [ -f "$candidate" ]; then IDENTITY_PATH="$candidate" break fi done local REPO_ROOT="" if [ -n "$IDENTITY_PATH" ] && command -v jq >/dev/null 2>&1; then REPO_ROOT=$(jq -r '.claudetools_root // empty' "$IDENTITY_PATH" 2>/dev/null) fi if [ -z "$REPO_ROOT" ]; then REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true) fi if [ -z "$REPO_ROOT" ] || [ ! -d "$REPO_ROOT/.git" ]; then echo -e "${RED}[ERROR]${NC} Cannot locate git repo. Add 'claudetools_root' to identity.json" >&2 exit 3 fi echo "$REPO_ROOT" } load_identity() { local REPO_ROOT="$1" if [ ! -f "$REPO_ROOT/.claude/identity.json" ]; then echo -e "${RED}[ERROR]${NC} identity.json not found" >&2 exit 2 fi # Detect Python local PYTHON="" if command -v jq >/dev/null 2>&1; then PYTHON=$(jq -r '.python.command // empty' "$REPO_ROOT/.claude/identity.json" 2>/dev/null) fi if [ -z "$PYTHON" ]; then for candidate in py python3 python; do if command -v "$candidate" >/dev/null 2>&1; then if "$candidate" -c "import sys; sys.exit(0)" >/dev/null 2>&1; then PYTHON="$candidate" break fi fi done fi if [ -z "$PYTHON" ]; then echo -e "${RED}[ERROR]${NC} No Python interpreter found" >&2 exit 2 fi # Load identity fields USER_DISPLAY=$($PYTHON -c "import json; d=json.load(open('$REPO_ROOT/.claude/identity.json')); print(d.get('full_name', d.get('user','unknown')))" 2>/dev/null || echo "unknown") USER_EMAIL=$($PYTHON -c "import json; d=json.load(open('$REPO_ROOT/.claude/identity.json')); print(d.get('email',''))" 2>/dev/null || echo "") export PYTHON USER_DISPLAY USER_EMAIL } reconcile_git_identity() { local want_name="$1" want_email="$2" [ "$want_name" = "unknown" ] && return 0 if [ -n "$want_name" ]; then local cur=$(git config user.name 2>/dev/null || true) if [ "$cur" != "$want_name" ]; then echo -e "${YELLOW}[WARNING]${NC} git user.name '$cur' -> '$want_name'" >&2 git config user.name "$want_name" fi fi if [ -n "$want_email" ]; then local cur=$(git config user.email 2>/dev/null || true) if [ "$cur" != "$want_email" ]; then echo -e "${YELLOW}[WARNING]${NC} git user.email '$cur' -> '$want_email'" >&2 git config user.email "$want_email" fi fi } resolve_submodule_collisions() { [ -f ".gitmodules" ] || return 1 local moved=0 subpath target untracked stamp dest local subpaths=() stamp=$(date -u "+%Y%m%dT%H%M%SZ") while read -r p; do [ -n "$p" ] && subpaths+=("$p") done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null | awk '{print $2}') for subpath in "${subpaths[@]}"; do [ -e "$subpath/.git" ] || continue target=$(git ls-tree HEAD -- "$subpath" 2>/dev/null | awk '$2=="commit"{print $3}') [ -n "$target" ] || continue git -C "$subpath" cat-file -e "$target" 2>/dev/null || continue while IFS= read -r -d '' untracked; do git -C "$subpath" cat-file -e "${target}:${untracked}" 2>/dev/null || continue dest="${untracked}.synced-aside-${stamp}" if mv "$subpath/$untracked" "$subpath/$dest" 2>/dev/null; then echo -e "${YELLOW}[WARNING]${NC} ${subpath}: preserved colliding '$untracked' as '$dest'" >&2 moved=1 fi done < <(git -C "$subpath" ls-files --others --exclude-standard -z 2>/dev/null) done [ "$moved" -eq 1 ] && return 0 || return 1 } inject_credentials() { # Inject parent HTTPS credentials to submodule URLs local PARENT_URL="$(git config --get remote.origin.url)" local CRED_HOST="" case "$PARENT_URL" in https://*@*) CRED_HOST="$(printf '%s' "$PARENT_URL" | sed -E 's#^(https://[^/]+)/.*#\1#')" ;; esac [ -z "$CRED_HOST" ] && return 0 while read -r pkey ppath; do [ -z "$ppath" ] && continue local name="$(printf '%s' "$pkey" | sed -E 's#^submodule\.(.*)\.path$#\1#')" local gm_url="$(git config --file .gitmodules --get "submodule.${name}.url")" case "$gm_url" in https://*) local sub_path="$(printf '%s' "$gm_url" | sed -E 's#^https://[^/]+(/.*)#\1#')" git config "submodule.${name}.url" "${CRED_HOST}${sub_path}" ;; esac done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null) } # --- Commands --- cmd_submodule_init() { local target_path="$1" if [ ! -f ".gitmodules" ]; then echo -e "${GREEN}[OK]${NC} No submodules configured" return 0 fi inject_credentials local count=0 while read -r pkey ppath; do [ -z "$ppath" ] && continue if [ -n "$target_path" ] && [ "$ppath" != "$target_path" ]; then continue fi local name="$(printf '%s' "$pkey" | sed -E 's#^submodule\.(.*)\.path$#\1#')" echo -e "${CYAN}[INFO]${NC} Initializing $ppath..." git submodule init -- "$ppath" >/dev/null 2>&1 set +e git submodule update --init -- "$ppath" >/dev/null 2>&1 local rc=$? set -e if [ $rc -ne 0 ]; then if resolve_submodule_collisions; then git submodule update --init -- "$ppath" >/dev/null 2>&1 fi fi if [ -d "$ppath" ] && [ "$USER_DISPLAY" != "unknown" ]; then (cd "$ppath" && reconcile_git_identity "$USER_DISPLAY" "$USER_EMAIL") || true fi count=$((count + 1)) done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$') echo -e "${GREEN}[OK]${NC} Initialized $count submodule(s)" } cmd_submodule_update() { local target_path="$1" if [ ! -f ".gitmodules" ]; then echo -e "${GREEN}[OK]${NC} No submodules" return 0 fi if [ -n "$target_path" ]; then echo -e "${CYAN}[INFO]${NC} Updating $target_path to pinned commit..." set +e local out=$(git submodule update --init -- "$target_path" 2>&1) local rc=$? set -e if [ $rc -ne 0 ]; then if resolve_submodule_collisions; then out=$(git submodule update --init -- "$target_path" 2>&1) rc=$? fi if [ $rc -ne 0 ]; then echo "$out" | grep -v '^$' echo -e "${RED}[ERROR]${NC} Submodule update failed" return 1 fi fi echo -e "${GREEN}[OK]${NC} Updated $target_path" else echo -e "${CYAN}[INFO]${NC} Updating all submodules..." set +e local out=$(git submodule update --init --recursive 2>&1) local rc=$? set -e if [ $rc -ne 0 ]; then if resolve_submodule_collisions; then out=$(git submodule update --init --recursive 2>&1) rc=$? fi if [ $rc -ne 0 ]; then echo "$out" | grep -v '^$' echo -e "${YELLOW}[WARNING]${NC} Some submodules failed to update" else echo -e "${GREEN}[OK]${NC} Updated all submodules (resolved collisions)" fi else echo -e "${GREEN}[OK]${NC} Updated all submodules" fi fi } cmd_submodule_sync() { local target_path="$1" if [ ! -f ".gitmodules" ]; then echo -e "${GREEN}[OK]${NC} No submodules" return 0 fi echo -e "${CYAN}[INFO]${NC} Syncing submodules to remote tips (fetch + ff-merge)..." if [ -n "$target_path" ]; then if [ ! -d "$target_path" ]; then echo -e "${RED}[ERROR]${NC} Submodule not initialized: $target_path" return 1 fi ( cd "$target_path" git fetch origin --quiet 2>/dev/null git checkout main --quiet 2>/dev/null || git checkout master --quiet 2>/dev/null git merge --ff-only origin/main --quiet 2>/dev/null || \ git merge --ff-only origin/master --quiet 2>/dev/null ) echo -e "${GREEN}[OK]${NC} Synced $target_path" else set +e git submodule foreach --quiet ' git fetch origin --quiet 2>/dev/null git checkout main --quiet 2>/dev/null || git checkout master --quiet 2>/dev/null git merge --ff-only origin/main --quiet 2>/dev/null || \ git merge --ff-only origin/master --quiet 2>/dev/null ' 2>/dev/null set -e echo -e "${GREEN}[OK]${NC} Synced all submodules to remote tips" fi } cmd_submodule_status() { if [ ! -f ".gitmodules" ]; then echo -e "${GREEN}[OK]${NC} No submodules" return 0 fi echo -e "${CYAN}Submodule Status:${NC}" git submodule status | while IFS= read -r line; do # Format: {status_char}{40_char_hash} {path} ({optional_description}) local status_char="${line:0:1}" local hash_and_rest="${line:1}" local path_and_desc="${hash_and_rest:41}" # Skip 40-char hash + 1 space case "$status_char" in "-") echo -e " ${YELLOW}[-]${NC} $path_and_desc (not initialized)" ;; "+") echo -e " ${YELLOW}[+]${NC} $path_and_desc (modified/ahead)" ;; "U") echo -e " ${RED}[U]${NC} $path_and_desc (merge conflict)" ;; " ") echo -e " ${GREEN}[OK]${NC} $path_and_desc" ;; *) echo -e " ${CYAN}[?]${NC} $path_and_desc (unknown status: $status_char)" ;; esac done } cmd_submodule_fix() { local target_path="$1" echo -e "${CYAN}[INFO]${NC} Running submodule diagnostics..." if [ ! -f ".gitmodules" ]; then echo -e "${GREEN}[OK]${NC} No submodules" return 0 fi local fixed=0 # Check for missing/uninitialized submodules while read -r pkey ppath; do [ -z "$ppath" ] && continue if [ -n "$target_path" ] && [ "$ppath" != "$target_path" ]; then continue fi if [ ! -d "$ppath/.git" ]; then echo -e "${YELLOW}[FIX]${NC} Initializing missing submodule: $ppath" cmd_submodule_init "$ppath" fixed=1 else # Check for detached HEAD if ! git -C "$ppath" symbolic-ref HEAD >/dev/null 2>&1; then echo -e "${YELLOW}[FIX]${NC} Reattaching detached HEAD in $ppath" ( cd "$ppath" git checkout main 2>/dev/null || git checkout master 2>/dev/null || true ) fixed=1 fi # Check for uncommitted changes if [ -n "$(git -C "$ppath" status --porcelain)" ]; then echo -e "${YELLOW}[WARNING]${NC} $ppath has uncommitted changes (not auto-fixed)" fi fi done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$') # Try update to resolve pointer mismatches if cmd_submodule_update "$target_path" 2>/dev/null; then fixed=1 fi if [ $fixed -eq 1 ]; then echo -e "${GREEN}[OK]${NC} Fixed submodule issues" else echo -e "${GREEN}[OK]${NC} No issues found" fi } cmd_status() { local verbose="$1" echo -e "${CYAN}Repository Status:${NC}" git status --short if [ "$verbose" = "--verbose" ]; then echo "" cmd_submodule_status fi } cmd_health() { echo -e "${CYAN}Running health check...${NC}" echo "" local issues=0 # Check identity if [ "$USER_DISPLAY" = "unknown" ]; then echo -e "${RED}[ERROR]${NC} identity.json unreadable" issues=$((issues + 1)) else echo -e "${GREEN}[OK]${NC} Identity: $USER_DISPLAY <$USER_EMAIL>" fi # Check git config local cfg_name=$(git config user.name || echo "") local cfg_email=$(git config user.email || echo "") if [ "$cfg_name" != "$USER_DISPLAY" ] || [ "$cfg_email" != "$USER_EMAIL" ]; then echo -e "${YELLOW}[WARNING]${NC} Git config doesn't match identity.json" issues=$((issues + 1)) else echo -e "${GREEN}[OK]${NC} Git identity matches" fi # Check for uncommitted changes if [ -n "$(git status --porcelain)" ]; then echo -e "${YELLOW}[INFO]${NC} Uncommitted changes present" else echo -e "${GREEN}[OK]${NC} Working tree clean" fi # Check submodules if [ -f ".gitmodules" ]; then local sub_issues=0 while read -r line; do local status="${line:0:1}" case "$status" in "-"|"U") sub_issues=$((sub_issues + 1)) ;; esac done < <(git submodule status 2>/dev/null) if [ $sub_issues -gt 0 ]; then echo -e "${YELLOW}[WARNING]${NC} $sub_issues submodule(s) need attention" issues=$((issues + 1)) else echo -e "${GREEN}[OK]${NC} All submodules healthy" fi fi # Check remote connectivity if git ls-remote origin HEAD >/dev/null 2>&1; then echo -e "${GREEN}[OK]${NC} Remote reachable" else echo -e "${RED}[ERROR]${NC} Cannot reach remote" issues=$((issues + 1)) fi echo "" if [ $issues -eq 0 ]; then echo -e "${GREEN}[SUCCESS]${NC} Repository healthy" return 0 else echo -e "${YELLOW}[WARNING]${NC} $issues issue(s) found" return 1 fi } cmd_fetch() { local recurse="$1" echo -e "${CYAN}[INFO]${NC} Fetching from origin..." if [ "$recurse" = "--recurse" ]; then git fetch origin --recurse-submodules else git fetch origin --no-recurse-submodules fi echo -e "${GREEN}[OK]${NC} Fetch complete" } cmd_pull() { local recurse="$1" echo -e "${CYAN}[INFO]${NC} Pulling from origin (rebase)..." local flags="--rebase" [ "$recurse" != "--recurse" ] && flags="$flags --no-recurse-submodules" if git pull origin main $flags; then echo -e "${GREEN}[OK]${NC} Pull successful" if [ "$recurse" != "--recurse" ]; then cmd_submodule_update fi else echo -e "${RED}[ERROR]${NC} Pull failed (likely conflicts)" return 1 fi } cmd_push() { echo -e "${CYAN}[INFO]${NC} Pushing to origin..." if git push origin main; then echo -e "${GREEN}[OK]${NC} Push successful" else echo -e "${RED}[ERROR]${NC} Push failed" return 1 fi } cmd_commit() { local msg="$1" if [ -z "$msg" ]; then echo -e "${RED}[ERROR]${NC} Commit message required" return 1 fi if git diff-index --quiet --cached HEAD -- 2>/dev/null; then echo -e "${YELLOW}[INFO]${NC} No staged changes to commit" return 0 fi git commit -m "$msg" echo -e "${GREEN}[OK]${NC} Committed" } cmd_verify_identity() { reconcile_git_identity "$USER_DISPLAY" "$USER_EMAIL" echo -e "${GREEN}[OK]${NC} Identity verified and updated if needed" } cmd_inject_creds() { inject_credentials echo -e "${GREEN}[OK]${NC} Credentials injected to submodules" } # --- Main --- COMMAND="${1:-}" shift || true # Change to repo root REPO_ROOT=$(get_repo_root) cd "$REPO_ROOT" # Load identity load_identity "$REPO_ROOT" case "$COMMAND" in submodule) SUBCOMMAND="${1:-}" shift || true case "$SUBCOMMAND" in init) cmd_submodule_init "$@" ;; update) cmd_submodule_update "$@" ;; sync) cmd_submodule_sync "$@" ;; status) cmd_submodule_status ;; fix) cmd_submodule_fix "$@" ;; *) echo "Usage: $0 submodule {init|update|sync|status|fix} [PATH]" exit 1 ;; esac ;; status) cmd_status "$@" ;; health) cmd_health ;; fetch) cmd_fetch "$@" ;; pull) cmd_pull "$@" ;; push) cmd_push ;; commit) cmd_commit "$@" ;; verify-identity) cmd_verify_identity ;; inject-creds) cmd_inject_creds ;; *) echo "Usage: $0 {submodule|status|health|fetch|pull|push|commit|verify-identity|inject-creds}" exit 1 ;; esac