#!/usr/bin/env bash # gitea — your friendly neighborhood Gitea CLI # It creates repos. It pushes code. It occasionally judges your commit messages. # Usage: gitea [options] set -euo pipefail cmd_help() { cat <<'EOF' gitea — your friendly neighborhood Gitea CLI Usage: gitea [options] Commands: create [--private] [--description "..."] Bring a new repo into this world remote Wire up git origin with token auth push Yeet current branch to origin delete Send a repo into the void (confirms first) list Show your repos (sorted by last updated) Examples: gitea create my-project --private --description "Top secret stuff" gitea remote my-project gitea push Config files (~/.config/gitea/): token Your API token (required) username Your Gitea username (required) host Gitea host override (default: git.sethpc.xyz) First time? Run /gitea-setup in Claude Code to get started. EOF } # Short-circuit help/unknown before loading config case "${1:-help}" in help|--help|-h) cmd_help; exit 0 ;; create|remote|push|delete|list|ls) ;; *) echo "Unknown command: $1 (try 'gitea help' — I promise it's helpful)" >&2; exit 1 ;; esac # --- Config --- GITEA_HOST="git.sethpc.xyz" if [[ -f "$HOME/.config/gitea/host" ]]; then GITEA_HOST="$(< "$HOME/.config/gitea/host")" GITEA_HOST="${GITEA_HOST%%[[:space:]]}" fi GITEA_USER="" if [[ -f "$HOME/.config/gitea/username" ]]; then GITEA_USER="$(< "$HOME/.config/gitea/username")" GITEA_USER="${GITEA_USER%%[[:space:]]}" fi [[ -z "$GITEA_USER" ]] && { echo "ERROR: No username found. Run /gitea-setup or put your username in ~/.config/gitea/username" >&2; exit 1; } GITEA_TOKEN="${GITEA_TOKEN:-}" if [[ -z "$GITEA_TOKEN" && -f "$HOME/.config/gitea/token" ]]; then GITEA_TOKEN="$(< "$HOME/.config/gitea/token")" GITEA_TOKEN="${GITEA_TOKEN%%[[:space:]]}" fi [[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: No token found. Run /gitea-setup or put your token in ~/.config/gitea/token" >&2; exit 1; } API_BASE="https://${GITEA_HOST}/api/v1" # --- Helpers --- api() { local method="$1" endpoint="$2" shift 2 curl -sf -X "$method" "${API_BASE}${endpoint}" \ -H "Content-Type: application/json" \ -H "Authorization: token ${GITEA_TOKEN}" \ "$@" } # --- Commands --- cmd_create() { local name="" private=true description="" while [[ $# -gt 0 ]]; do case "$1" in --private|-p) private=true; shift ;; --public) private=false; shift ;; --description|-d) description="$2"; shift 2 ;; -*) echo "Unknown option: $1 (I don't know what that means and I'm too proud to guess)" >&2; exit 1 ;; *) name="$1"; shift ;; esac done [[ -z "$name" ]] && { echo "Usage: gitea create [--private] [--description \"...\"]"; echo " (You forgot the name. The repo needs a name. We all need names.)" >&2; exit 1; } local payload payload=$(jq -n \ --arg name "$name" \ --argjson private "$private" \ --arg desc "$description" \ '{name: $name, private: $private, description: $desc, auto_init: false}') local resp resp=$(api POST "/user/repos" -d "$payload") || { echo "FAIL: Gitea said no. Check your token and network." >&2; exit 1; } local html_url html_url=$(echo "$resp" | jq -r '.html_url') echo "Created: ${html_url}" echo "Clone: https://${GITEA_HOST}/${GITEA_USER}/${name}.git" echo "" echo "Next up: 'gitea remote ${name}' to wire up your origin. You know the drill." } cmd_remote() { local name="${1:-}" [[ -z "$name" ]] && { echo "Usage: gitea remote "; echo " (Which repo? I can't read minds. Yet.)" >&2; exit 1; } local url="https://${GITEA_HOST}/${GITEA_USER}/${name}.git" git config credential.helper 'store' local cred_file="${HOME}/.git-credentials" local cred_entry="https://${GITEA_USER}:${GITEA_TOKEN}@${GITEA_HOST}" if ! grep -qF "${GITEA_HOST}" "$cred_file" 2>/dev/null; then echo "$cred_entry" >> "$cred_file" chmod 600 "$cred_file" fi if git remote get-url origin &>/dev/null; then git remote set-url origin "$url" echo "Updated origin -> ${url}" else git remote add origin "$url" echo "Added origin -> ${url}" fi echo "You're locked and loaded. 'gitea push' when ready." } cmd_push() { local branch branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || { echo "This isn't a git repo. I checked." >&2; exit 1; } echo "Pushing ${branch} to origin... hold onto your bits." git push -u origin "$branch" } cmd_delete() { local name="${1:-}" [[ -z "$name" ]] && { echo "Usage: gitea delete "; echo " (Delete what? Be specific. Destruction requires precision.)" >&2; exit 1; } read -rp "Delete ${GITEA_USER}/${name}? This is permanent. No take-backs. [y/N] " confirm [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Crisis averted. Repo lives another day."; exit 0; } if api DELETE "/repos/${GITEA_USER}/${name}"; then echo "Deleted: ${GITEA_USER}/${name}. It's gone. Pour one out." else echo "FAIL: Could not delete ${GITEA_USER}/${name}. Maybe it's already gone? Maybe it's fighting back?" >&2; exit 1 fi } cmd_list() { local resp resp=$(api GET "/user/repos?limit=50&sort=updated") || { echo "FAIL: Couldn't fetch repos. Is Gitea awake?" >&2; exit 1; } echo "$resp" | jq -r '.[] | "\(.name)\t\(if .private then "private" else "public" end)\t\(.description // "")"' | column -t -s$'\t' } case "${1:-help}" in create) shift; cmd_create "$@" ;; remote) shift; cmd_remote "$@" ;; push) shift; cmd_push "$@" ;; delete) shift; cmd_delete "$@" ;; list|ls) shift; cmd_list "$@" ;; esac