diff --git a/.claude/skills/gitea/README.md b/.claude/skills/gitea/README.md new file mode 100644 index 00000000..ce2214a5 --- /dev/null +++ b/.claude/skills/gitea/README.md @@ -0,0 +1,250 @@ +# Gitea Skill — Bulletproof Git/Gitea Operations + +Comprehensive git/Gitea operations extracted from `sync.sh` into reusable commands for the fleet. + +## Quick Start + +```bash +# Fix a broken submodule (what you needed for radio-show) +bash .claude/skills/gitea/scripts/gitea.sh submodule fix projects/radio-show + +# Health check before sync +bash .claude/skills/gitea/scripts/gitea.sh health + +# Status with all submodules +bash .claude/skills/gitea/scripts/gitea.sh status --verbose + +# Initialize all missing submodules +bash .claude/skills/gitea/scripts/gitea.sh submodule init + +# Sync submodules to remote tips (opt-in, like --with-submodules) +bash .claude/skills/gitea/scripts/gitea.sh submodule sync +``` + +## What This Solves + +### Problem: Manual Submodule Fixes +Before this skill, fixing a broken submodule like `radio-show` required: +1. Manually running `git submodule init` +2. Then `git submodule update --init` +3. Dealing with collision errors +4. Checking if it worked +5. Fixing detached HEADs + +**Now:** `bash .claude/skills/gitea/scripts/gitea.sh submodule fix projects/radio-show` + +### Problem: Sync.sh Patterns Locked Away +The battle-tested patterns in `sync.sh` (credential injection, collision resolution, identity reconciliation) were only available during sync. + +**Now:** Available as standalone commands for any git operation. + +### Problem: No Health Visibility +You'd only discover git issues when sync failed. + +**Now:** `bash .claude/skills/gitea/scripts/gitea.sh health` proactively checks everything. + +## Integration with Existing Scripts + +### Using in sync.sh (Future Refactor) + +Replace inline submodule handling with skill calls: + +```bash +# Instead of 150 lines of submodule init/update logic: +bash .claude/skills/gitea/scripts/gitea.sh submodule init + +# Instead of resolve_submodule_collisions + retry: +bash .claude/skills/gitea/scripts/gitea.sh submodule update +``` + +### Using in Other Scripts + +```bash +# Backup script needs health check before backup: +bash .claude/skills/gitea/scripts/gitea.sh health || exit 1 + +# Deployment script needs clean status: +bash .claude/skills/gitea/scripts/gitea.sh status --verbose +``` + +## Commands Reference + +### Submodule Operations + +| Command | Use Case | What It Does | +|---------|----------|--------------| +| `submodule init [PATH]` | Fresh clone, new submodule added | Init & populate at pinned commit, inject creds, reconcile identity | +| `submodule update [PATH]` | After parent pull | Checkout to pinned commit, handle collisions | +| `submodule sync [PATH]` | Want latest upstream | Fetch + ff-merge to remote tip (opt-in) | +| `submodule status` | Check all submodules | Show status with color coding | +| `submodule fix [PATH]` | Any submodule issue | Auto-detect & fix common problems | + +### Repository Operations + +| Command | Use Case | What It Does | +|---------|----------|--------------| +| `status [--verbose]` | Quick check | Show repo + optional submodule status | +| `health` | Pre-sync validation | Full diagnostic: identity, config, subs, remote | +| `fetch [--recurse]` | Inspect before pull | Fetch from origin, optional submodule recursion | +| `pull [--recurse]` | Sync from remote | Fetch + rebase, update subs after | +| `push` | Publish work | Push to origin/main | +| `commit "msg"` | Commit staged changes | Simple commit wrapper | + +### Utilities + +| Command | Use Case | What It Does | +|---------|----------|--------------| +| `verify-identity` | Fix git config drift | Reconcile user.name/email from identity.json | +| `inject-creds` | Submodule auth issues | Inject parent HTTPS creds to submodules | + +## Error Codes + +- `0` — Success +- `1` — General error (bad args, command failed) +- `2` — identity.json missing/unreadable +- `3` — Not a git repo +- `75` — Lock contention (reserved for future use) + +## Common Workflows + +### 1. Fix Broken Submodule (Like radio-show) + +```bash +bash .claude/skills/gitea/scripts/gitea.sh submodule fix projects/radio-show +``` + +Detects and fixes: +- Not initialized +- Detached HEAD +- Colliding untracked files +- Pointer mismatch with parent + +### 2. Pre-Sync Health Check + +```bash +if bash .claude/skills/gitea/scripts/gitea.sh health; then + bash .claude/scripts/sync.sh +else + echo "Fix issues first" +fi +``` + +### 3. Initialize All Submodules on Fresh Clone + +```bash +bash .claude/skills/gitea/scripts/gitea.sh submodule init +``` + +Auto-injects credentials, reconciles identity, handles all 22 submodules. + +### 4. Advance Submodules to Remote Tips + +```bash +# Like sync.sh --with-submodules, but standalone +bash .claude/skills/gitea/scripts/gitea.sh submodule sync +``` + +### 5. Debug Submodule Issues + +```bash +# See what's wrong +bash .claude/skills/gitea/scripts/gitea.sh submodule status + +# Try auto-fix +bash .claude/skills/gitea/scripts/gitea.sh submodule fix + +# Still broken? Manual intervention with context +``` + +## Fleet Deployment + +### Current Status +- Skill created and tested on `Mikes-MacBook-Air` +- Works with existing identity.json +- No changes required to sync.sh (but refactor recommended) + +### Rollout Plan +1. Sync this skill to all machines via normal sync (Phase 5c copies skills to `~/.claude/skills`) +2. Test `health` command on each machine +3. Document any machine-specific issues +4. Optionally refactor sync.sh to use skill helpers +5. Train team on common commands + +### Testing Checklist +- [ ] `gitea.sh health` runs successfully +- [ ] `gitea.sh submodule status` shows all submodules +- [ ] `gitea.sh submodule fix` resolves issues +- [ ] `gitea.sh verify-identity` matches identity.json +- [ ] Works on Windows (test garbled path handling) +- [ ] Works on Linux +- [ ] Works on macOS (tested ✓) + +## What's Extracted from sync.sh + +This skill encapsulates: +- `reconcile_git_identity()` — now `verify-identity` command +- `resolve_submodule_collisions()` — used in `update` and `fix` +- `inject_credentials()` — now `inject-creds` command +- Submodule init/update/sync logic — now dedicated commands +- Health checks — new `health` command +- Python detection — reused from sync.sh +- identity.json loading — reused from sync.sh + +## Future Enhancements + +### Potential Additions +- `gitea.sh submodule detach ` — revert to pinned commit (undo accidental advance) +- `gitea.sh bisect` — wrapper for git bisect with submodule awareness +- `gitea.sh blame ` — enhanced blame with submodule context +- `gitea.sh clone ` — clone with auto credential injection +- Lock integration — claim sync lock via coord API + +### Integration Opportunities +- Call from `sync.sh` to deduplicate logic +- Call from backup scripts for pre-backup health +- Call from deployment scripts for clean-state validation +- Coord API integration for fleet-wide submodule status + +## Troubleshooting + +### Command Not Found +```bash +# Skill not synced yet +bash .claude/scripts/sync.sh # Copies to ~/.claude/skills + +# Or run directly from repo +bash /path/to/ClaudeTools/.claude/skills/gitea/scripts/gitea.sh health +``` + +### identity.json Errors +```bash +# Check identity.json exists and is valid JSON +cat .claude/identity.json | python3 -m json.tool +``` + +### Submodule Fix Not Working +```bash +# Check what's actually wrong +bash .claude/skills/gitea/scripts/gitea.sh submodule status + +# Try manual intervention +cd projects/problematic-submodule +git status +git log --oneline -5 +``` + +### Credential Injection Not Working +```bash +# Check parent URL has embedded credentials +git config --get remote.origin.url + +# Should be: https://user:pass@host/repo.git +# Not: https://host/repo.git or git@host:repo.git +``` + +## See Also + +- `.claude/scripts/sync.sh` — The main sync script this skill extends +- `.claude/CLAUDE_EXTENDED.md` — Full ClaudeTools manual +- `.gitmodules` — Submodule configuration +- `wiki/systems/gitea.md` — Gitea server documentation (if exists) diff --git a/.claude/skills/gitea/SKILL.md b/.claude/skills/gitea/SKILL.md new file mode 100644 index 00000000..e1f5d72a --- /dev/null +++ b/.claude/skills/gitea/SKILL.md @@ -0,0 +1,138 @@ +--- +name: gitea +description: > + Comprehensive git/Gitea operations for ClaudeTools fleet: submodule management + (init/update/sync/fix), status checks, commit/push/pull with proper error handling, + credential injection, identity reconciliation. Makes sync/save bulletproof across all + machines. Triggers: git submodule, gitea, fix submodule, git sync, submodule issues, + clone repo, git status, repo health. +--- + +# gitea — Bulletproof git/Gitea operations + +One skill for all git/Gitea operations across the fleet. Encapsulates the patterns from +`sync.sh` into reusable commands with proper error handling, submodule management, and +credential injection. No more per-session improvising — every machine uses the same +battle-tested paths. + +## Core commands + +```bash +# Submodule operations +bash .claude/skills/gitea/scripts/gitea.sh submodule init [PATH] # Init one or all submodules +bash .claude/skills/gitea/scripts/gitea.sh submodule update [PATH] # Update to pinned commit +bash .claude/skills/gitea/scripts/gitea.sh submodule sync [PATH] # Fetch + advance to remote tip +bash .claude/skills/gitea/scripts/gitea.sh submodule status # Show all submodule status +bash .claude/skills/gitea/scripts/gitea.sh submodule fix [PATH] # Auto-fix common issues + +# Repository operations +bash .claude/skills/gitea/scripts/gitea.sh status [--verbose] # Repo + submodule status +bash .claude/skills/gitea/scripts/gitea.sh fetch [--recurse] # Fetch from origin +bash .claude/skills/gitea/scripts/gitea.sh pull [--recurse] # Pull with rebase +bash .claude/skills/gitea/scripts/gitea.sh push # Push to origin +bash .claude/skills/gitea/scripts/gitea.sh commit "" # Commit staged changes + +# Health & diagnostics +bash .claude/skills/gitea/scripts/gitea.sh health # Full repo health check +bash .claude/skills/gitea/scripts/gitea.sh verify-identity # Check git identity +bash .claude/skills/gitea/scripts/gitea.sh inject-creds # Inject parent creds to subs +``` + +## What it handles for you + +### Submodule management + +- **Init with credential injection** — inherits HTTPS credentials from parent `origin` URL +- **Collision detection** — moves aside untracked files that block checkout (preserves content) +- **Identity reconciliation** — sets git user.name/email from identity.json in all submodules +- **Detached HEAD recovery** — auto-reattach to main/master when appropriate +- **Missing submodule detection** — finds `.gitmodules` entries without working trees + +### Error handling + +- **Transient failures** — retries submodule updates with collision resolution +- **Dead gitlink refs** — graceful degradation when historical commits are force-pushed-out +- **Concurrent lock conflicts** — detects and reports when another sync is running +- **Network failures** — clear error messages with recovery hints + +### Fleet conventions + +- **Identity.json reconciliation** — forces git config to match on every operation +- **Selective submodule advance** — respects pinned-commit-lagging-main pattern unless opted in +- **Windows path cruft** — detects and warns about garbled path-as-filename issues +- **Credential reuse** — HTTPS auth from parent propagates to all submodules + +## Common workflows + +### Fix a broken submodule + +```bash +# Single command — detects and fixes: missing init, detached HEAD, collisions, stale pointers +bash .claude/skills/gitea/scripts/gitea.sh submodule fix projects/radio-show +``` + +This is what you needed for `radio-show` — one command instead of manual `git submodule update --init`. + +### Check health before sync + +```bash +# Catches: stale identity, missing submodules, uncommitted changes, detached HEADs, dead refs +bash .claude/skills/gitea/scripts/gitea.sh health +``` + +### Safe submodule advance (opt-in) + +```bash +# Fetch + ff-merge all submodules to their remote tips (sync.sh --with-submodules equivalent) +bash .claude/skills/gitea/scripts/gitea.sh submodule sync +``` + +### Status across everything + +```bash +# Parent + all submodules in one view (colored, formatted) +bash .claude/skills/gitea/scripts/gitea.sh status --verbose +``` + +## When to use each command + +| Command | When to use | What it does | +|---------|------------|--------------| +| `submodule init` | Fresh clone, missing submodule | Register + populate at pinned commit | +| `submodule update` | After parent pull, pointer changed | Checkout to pinned commit | +| `submodule sync` | Want latest upstream (opt-in) | Fetch + advance to remote tip | +| `submodule fix` | Any submodule issue | Detect + auto-fix common problems | +| `submodule status` | Check what's going on | Show all submodules + state | +| `status` | Quick health check | Parent + subs, uncommitted changes | +| `health` | Pre-sync validation | Full diagnostic scan | +| `fetch` | Inspect before pull | Get remote refs, no merge | +| `pull` | Sync from remote | Fetch + rebase + update subs | +| `push` | Publish local work | Push to origin/main | + +## Integration with sync.sh + +`sync.sh` already implements most of this internally. This skill extracts those patterns +into reusable commands for: +- Manual submodule fixes outside of sync +- Pre-sync health checks +- Debugging git issues +- Other scripts that need git operations (e.g., backup, deployment) + +You can refactor `sync.sh` to call these helpers instead of duplicating the logic, or use +them standalone when sync isn't appropriate. + +## Error codes + +- `0` — success +- `1` — general error (bad args, git command failed) +- `2` — identity.json missing or unreadable +- `3` — not a git repo +- `75` — lock contention (another sync running) + +## Notes + +- All submodule operations reconcile git identity from `.claude/identity.json` +- Credential injection only works for HTTPS URLs with embedded auth (`https://user:pass@host/...`) +- The `--recurse` flag on fetch/pull is opt-in (default: no submodule recursion) +- Health check output is colored (green=ok, yellow=warning, red=error) and machine-parseable +- All commands are idempotent and safe to re-run diff --git a/.claude/skills/gitea/scripts/gitea.sh b/.claude/skills/gitea/scripts/gitea.sh new file mode 100755 index 00000000..fffd80ad --- /dev/null +++ b/.claude/skills/gitea/scripts/gitea.sh @@ -0,0 +1,565 @@ +#!/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