Files
home-scripts/rpush
2026-05-20 00:07:52 -04:00

381 lines
9.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SERVER="git.nytegear.com"
OWNER="aargonian"
REMOTE="origin"
NAME=""
DESCRIPTION=""
PRIVATE=false
PROTOCOL="https"
BRANCH=""
INITIAL_COMMIT=false
EXISTING_OK=false
REPLACE_REMOTE=false
PUSH_TAGS=true
DRY_RUN=false
usage() {
cat <<'EOF'
Usage:
rpush [options]
Create a new Gitea repo for the current directory, add it as a Git remote,
and push the current branch.
Options:
--name NAME Remote repo name. Defaults to current directory name.
--description TEXT Repository description.
--public Create public repo. Default.
--private Create private repo. Default is public.
--remote NAME Git remote name. Default: origin.
--branch NAME Branch to push. Defaults to current branch.
--https Use HTTPS remote URL. Default.
--ssh Use SSH remote URL instead of SSH.
--initial-commit If the repo has no commits, git add -A and commit.
--existing-ok Continue if the Gitea repo already exists.
--replace-remote Replace local remote URL if the remote name exists.
--no-tags Do not push tags.
--dry-run Print what would happen, but do not create or push.
-h, --help Show this help.
Auth:
Set GITEA_TOKEN before running, or the script will prompt for it.
Examples:
rpush
rpush --name my-custom-repo
rpush --name my-custom-repo --public
rpush --initial-commit
EOF
}
die() {
printf 'rpush: error: %s\n' "$*" >&2
exit 1
}
info() {
printf 'rpush: %s\n' "$*" >&2
}
need() {
command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}
json_payload() {
python3 - "$NAME" "$DESCRIPTION" "$PRIVATE" "$BRANCH" <<'PY'
import json
import sys
name, description, private, branch = sys.argv[1:]
payload = {
"name": name,
"description": description,
"private": private.lower() == "true",
"auto_init": False,
}
if branch:
payload["default_branch"] = branch
print(json.dumps(payload, separators=(",", ":")))
PY
}
api_message() {
local file="$1"
python3 - "$file" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
text = path.read_text(errors="replace").strip()
try:
data = json.loads(text) if text else {}
except Exception:
print(text[:4000] if text else "(empty response)")
raise SystemExit
if isinstance(data, dict):
print(data.get("message") or json.dumps(data, indent=2))
else:
print(json.dumps(data, indent=2))
PY
}
parse_args() {
while (($#)); do
case "$1" in
--name)
[[ $# -ge 2 ]] || die "--name requires a value"
NAME="$2"
shift 2
;;
--description)
[[ $# -ge 2 ]] || die "--description requires a value"
DESCRIPTION="$2"
shift 2
;;
--public)
PRIVATE=false
shift
;;
--private)
PRIVATE=true
shift
;;
--remote)
[[ $# -ge 2 ]] || die "--remote requires a value"
REMOTE="$2"
shift 2
;;
--branch)
[[ $# -ge 2 ]] || die "--branch requires a value"
BRANCH="$2"
shift 2
;;
--https)
PROTOCOL="https"
shift
;;
--ssh)
PROTOCOL="ssh"
shift
;;
--initial-commit)
INITIAL_COMMIT=true
shift
;;
--existing-ok)
EXISTING_OK=true
shift
;;
--replace-remote)
REPLACE_REMOTE=true
shift
;;
--no-tags)
PUSH_TAGS=false
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
}
validate_repo_name() {
[[ -n "$NAME" ]] || die "repo name is empty"
[[ "$NAME" != "." && "$NAME" != ".." ]] || die "invalid repo name: $NAME"
[[ "$NAME" != *"/"* ]] || die "repo name must not contain /"
if [[ "$NAME" =~ [[:space:]] ]]; then
die "repo name contains whitespace; pass a cleaner --name"
fi
if [[ ! "$NAME" =~ ^[A-Za-z0-9._-]+$ ]]; then
die "repo name contains unsupported characters; use letters, numbers, dot, underscore, or dash"
fi
}
ensure_git_repo_root() {
local cwd root
cwd="$(pwd -P)"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
root="$(git rev-parse --show-toplevel)"
root="$(cd "$root" && pwd -P)"
[[ "$cwd" == "$root" ]] || die "current directory is inside a Git repo but not at its root: $root"
else
info "initializing Git repository"
if [[ -n "$BRANCH" ]]; then
git init -b "$BRANCH"
else
git init
fi
fi
}
ensure_commit_exists() {
if git rev-parse --verify HEAD >/dev/null 2>&1; then
return
fi
if [[ "$INITIAL_COMMIT" == true ]]; then
info "creating initial commit"
git add -A
git diff --cached --quiet && die "nothing staged; directory appears empty"
git commit -m "Initial commit"
else
die "repository has no commits. Commit manually first, or rerun with --initial-commit"
fi
}
detect_branch() {
if [[ -z "$BRANCH" ]]; then
BRANCH="$(git branch --show-current)"
fi
[[ -n "$BRANCH" ]] || die "could not determine branch; are you in detached HEAD?"
}
remote_url() {
case "$PROTOCOL" in
ssh)
printf 'git@%s:%s/%s.git\n' "$SERVER" "$OWNER" "$NAME"
;;
https)
printf 'https://%s/%s/%s.git\n' "$SERVER" "$OWNER" "$NAME"
;;
*)
die "unsupported protocol: $PROTOCOL"
;;
esac
}
create_gitea_repo() {
local api_url payload tmp_body tmp_cfg status
api_url="https://${SERVER}/api/v1/user/repos"
payload="$(json_payload)"
tmp_body="$(mktemp)"
tmp_cfg="$(mktemp)"
chmod 600 "$tmp_cfg"
{
printf 'header = "Accept: application/json"\n'
printf 'header = "Content-Type: application/json"\n'
printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN"
} > "$tmp_cfg"
if [[ "$DRY_RUN" == true ]]; then
info "would POST $api_url"
info "would create repo: $OWNER/$NAME private=$PRIVATE branch=$BRANCH"
rm -f "$tmp_body" "$tmp_cfg"
return
fi
if ! status="$(
curl --silent --show-error \
--output "$tmp_body" \
--write-out '%{http_code}' \
--request POST \
--config "$tmp_cfg" \
--data-binary "$payload" \
"$api_url"
)"; then
rm -f "$tmp_body" "$tmp_cfg"
die "curl failed while creating remote repo"
fi
case "$status" in
201)
info "created remote repo: $OWNER/$NAME"
;;
409)
if [[ "$EXISTING_OK" == true ]]; then
info "remote repo already exists; continuing because --existing-ok was set"
else
rm -f "$tmp_body" "$tmp_cfg"
die "remote repo already exists: $OWNER/$NAME. Use --existing-ok to push anyway"
fi
;;
*)
printf 'rpush: Gitea API returned HTTP %s: ' "$status" >&2
api_message "$tmp_body" >&2
rm -f "$tmp_body" "$tmp_cfg"
exit 1
;;
esac
rm -f "$tmp_body" "$tmp_cfg"
}
configure_remote() {
local url existing_url
url="$(remote_url)"
if git remote get-url "$REMOTE" >/dev/null 2>&1; then
existing_url="$(git remote get-url "$REMOTE")"
if [[ "$existing_url" == "$url" ]]; then
info "remote $REMOTE already points to $url"
return
fi
if [[ "$REPLACE_REMOTE" == true ]]; then
info "replacing remote $REMOTE URL"
[[ "$DRY_RUN" == true ]] || git remote set-url "$REMOTE" "$url"
return
fi
die "remote $REMOTE already exists with different URL: $existing_url. Use --replace-remote"
fi
info "adding remote $REMOTE -> $url"
[[ "$DRY_RUN" == true ]] || git remote add "$REMOTE" "$url"
}
push_repo() {
if [[ "$DRY_RUN" == true ]]; then
info "would push branch $BRANCH to $REMOTE"
[[ "$PUSH_TAGS" == true ]] && info "would push tags"
return
fi
info "pushing branch $BRANCH to $REMOTE"
git push -u "$REMOTE" "$BRANCH"
if [[ "$PUSH_TAGS" == true ]]; then
info "pushing tags to $REMOTE"
git push "$REMOTE" --tags
fi
}
main() {
parse_args "$@"
need git
need curl
need python3
if [[ -z "$NAME" ]]; then
NAME="$(basename "$(pwd -P)")"
fi
validate_repo_name
ensure_git_repo_root
ensure_commit_exists
detect_branch
if [[ -z "${GITEA_TOKEN:-}" && "$DRY_RUN" != true ]]; then
read -r -s -p "Gitea access token: " GITEA_TOKEN
printf '\n' >&2
fi
[[ "$DRY_RUN" == true || -n "${GITEA_TOKEN:-}" ]] || die "GITEA_TOKEN is empty"
create_gitea_repo
configure_remote
push_repo
info "done: $OWNER/$NAME"
}
main "$@"