381 lines
9.5 KiB
Bash
Executable File
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 "$@"
|