Previously repo create only initialized the local repo and relied on Gitea's push-to-create feature, which always yields a private repository. Now the script pre-creates the remote via the Gitea API honoring --visibility public|private (default: private), and falls back to a PATCH when the repo already exists.
461 lines
15 KiB
Bash
Executable File
461 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Get the directory where this script is located
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
MYSQL_CONTAINER="git-kc-mysql"
|
|
MYSQL_USER="root"
|
|
MYSQL_DB="giteadb"
|
|
MYSQL_ROOT_PASS="K0DprNKJ^vAu3Mx32hMZ%LCzWKElFRfA"
|
|
GIT_HOST="root@vps.git"
|
|
|
|
# ─────────────────────────────────────────────
|
|
# create
|
|
# ─────────────────────────────────────────────
|
|
do_create() {
|
|
local REPO_NAME="" VISIBILITY="private"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--visibility)
|
|
VISIBILITY="$2"; shift 2 ;;
|
|
-h|--help)
|
|
echo "Usage: $0 create <repo-name> [--visibility <public|private>]"
|
|
exit 0 ;;
|
|
*)
|
|
if [[ -z "$REPO_NAME" ]]; then
|
|
REPO_NAME="$1"; shift
|
|
else
|
|
echo "Error: unexpected argument '$1'"
|
|
exit 1
|
|
fi ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$REPO_NAME" ]]; then
|
|
echo "Usage: $0 create <repo-name> [--visibility <public|private>]"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$VISIBILITY" != "public" && "$VISIBILITY" != "private" ]]; then
|
|
echo "Error: --visibility must be 'public' or 'private' (got '$VISIBILITY')"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
|
echo "Error: script must be executed by pcexec"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate repo name
|
|
if ! [[ "$REPO_NAME" =~ ^[a-zA-Z0-9_.-]+$ ]]; then
|
|
echo "Error: Invalid repository name '$REPO_NAME'"
|
|
echo "Only alphanumeric, hyphens, underscores, and dots are allowed."
|
|
exit 1
|
|
fi
|
|
|
|
REPO_DIR="${AGENT_WORKSPACE}/${REPO_NAME}"
|
|
|
|
# Pre-create the repo on Gitea so --visibility is honoured. Relying on
|
|
# push-to-create always yields a private repo regardless of flag.
|
|
if ! secret-mgr list | grep -q "git-access-token"; then
|
|
echo "Error: git-access-token not found. Generate one first."
|
|
exit 1
|
|
fi
|
|
|
|
local USERNAME TOKEN PRIVATE_JSON STATUS BODY
|
|
USERNAME="$(secret-mgr get-username --key git)"
|
|
TOKEN="$(secret-mgr get-secret --key git-access-token)"
|
|
if [[ "$VISIBILITY" == "public" ]]; then
|
|
PRIVATE_JSON="false"
|
|
else
|
|
PRIVATE_JSON="true"
|
|
fi
|
|
|
|
echo "Creating ${USERNAME}/${REPO_NAME} on git.hangman-lab.top (visibility: ${VISIBILITY})..."
|
|
RESP=$(curl -s -w "
|
|
%{http_code}" -X POST \
|
|
-H "Authorization: token ${TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"name\": \"${REPO_NAME}\", \"private\": ${PRIVATE_JSON}, \"auto_init\": false}" \
|
|
"https://git.hangman-lab.top/api/v1/user/repos")
|
|
STATUS=$(printf "%s" "$RESP" | tail -n1)
|
|
BODY=$(printf "%s" "$RESP" | sed '$d')
|
|
|
|
case "$STATUS" in
|
|
201)
|
|
echo " Remote repository created."
|
|
;;
|
|
409)
|
|
echo " Remote repository already exists — ensuring visibility..."
|
|
PATCH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH \
|
|
-H "Authorization: token ${TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"private\": ${PRIVATE_JSON}}" \
|
|
"https://git.hangman-lab.top/api/v1/repos/${USERNAME}/${REPO_NAME}")
|
|
if [[ "$PATCH_STATUS" != "200" ]]; then
|
|
echo "Warning: failed to update visibility (HTTP $PATCH_STATUS)"
|
|
fi
|
|
;;
|
|
*)
|
|
echo "Error: remote repo creation failed (HTTP $STATUS): $BODY"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
mkdir -p "${REPO_DIR}"
|
|
cd "${REPO_DIR}"
|
|
if [[ ! -d .git ]]; then
|
|
git init
|
|
fi
|
|
|
|
REMOTE_URL="https://git.hangman-lab.top/${USERNAME}/${REPO_NAME}.git"
|
|
if ! git remote get-url origin >/dev/null 2>&1; then
|
|
git remote add origin "${REMOTE_URL}"
|
|
else
|
|
git remote set-url origin "${REMOTE_URL}"
|
|
fi
|
|
|
|
do_config --repo-path "${REPO_DIR}"
|
|
|
|
echo "Done! Repository created at: ${REPO_DIR}"
|
|
echo "Remote: ${REMOTE_URL}"
|
|
}
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# add-collaborators
|
|
# ─────────────────────────────────────────────
|
|
do_add_collaborators() {
|
|
local user="" repo=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--user) user="$2"; shift 2 ;;
|
|
--repo) repo="$2"; shift 2 ;;
|
|
*) echo "Unknown option: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$user" || -z "$repo" ]]; then
|
|
echo "Usage: $0 add-collaborators --user <user> --repo <repo>"
|
|
exit 1
|
|
fi
|
|
|
|
if ! secret-mgr list | grep -q "git-access-token"; then
|
|
echo "Error: git-access-token not found. Generate one first."
|
|
exit 1
|
|
fi
|
|
|
|
owner=$(secret-mgr get-username --key git)
|
|
token=$(secret-mgr get-secret --key git-access-token)
|
|
|
|
curl -s -X PUT \
|
|
-H "Authorization: token $token" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"permission":"write"}' \
|
|
"https://git.hangman-lab.top/api/v1/repos/$owner/$repo/collaborators/$user"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────
|
|
# list-all
|
|
# ─────────────────────────────────────────────
|
|
do_list_all() {
|
|
USERNAME=$(ego-mgr get default-username)
|
|
if [[ -z "$USERNAME" ]]; then
|
|
echo "Error: cannot get username from ego-mgr" >&2
|
|
exit 1
|
|
fi
|
|
|
|
QUERY="
|
|
SELECT r.name, u.name as owner, r.is_private,
|
|
COALESCE((r.owner_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
|
OR a.user_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
|
OR EXISTS (SELECT 1 FROM team_user tu JOIN team t ON t.id = tu.team_id
|
|
WHERE tu.uid = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
|
AND (t.includes_all_repositories = 1
|
|
OR EXISTS (SELECT 1 FROM team_repo tr WHERE tr.team_id = t.id AND tr.repo_id = r.id)))), 0) as can_write
|
|
FROM repository r
|
|
JOIN user u ON r.owner_id = u.id
|
|
LEFT JOIN access a ON a.repo_id = r.id AND a.user_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
|
WHERE r.is_archived = 0
|
|
AND (r.owner_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
|
OR r.is_private = 0
|
|
OR a.user_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
|
OR EXISTS (SELECT 1 FROM team_user tu WHERE tu.uid = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci))
|
|
ORDER BY r.name
|
|
"
|
|
|
|
RESULT=$(ssh -o StrictHostKeyChecking=no "$GIT_HOST" \
|
|
"docker exec $MYSQL_CONTAINER mysql -u $MYSQL_USER -p'$MYSQL_ROOT_PASS' -N -e \"$QUERY\" $MYSQL_DB" 2>/dev/null)
|
|
|
|
echo "| proj-name | owner | url | can-write |"
|
|
echo "|------------|-------|-----|-----------|"
|
|
|
|
[[ -z "$RESULT" ]] && exit 0
|
|
|
|
echo "$RESULT" | while IFS=$'\t' read -r name owner is_private can_write; do
|
|
can_write_val=$([[ "$can_write" == "1" ]] && echo "yes" || echo "no")
|
|
echo "| $name | $owner | https://git.hangman-lab.top/$owner/$name.git | $can_write_val |"
|
|
done
|
|
}
|
|
|
|
# ─────────────────────────────────────────────
|
|
# config
|
|
# ─────────────────────────────────────────────
|
|
do_config() {
|
|
local REPO_PATH=""
|
|
local RECURSIVE=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--repo-path) REPO_PATH="${2:-}"; shift 2 ;;
|
|
--recursive) RECURSIVE=true; shift ;;
|
|
*) echo "Usage: $0 config --repo-path <path> [--recursive]"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$REPO_PATH" ]]; then
|
|
echo "Usage: $0 config --repo-path <path> [--recursive]"
|
|
exit 1
|
|
fi
|
|
|
|
EMAIL=$(ego-mgr get email)
|
|
if [[ -z "$EMAIL" ]]; then
|
|
echo "Error: email not set in ego-mgr"
|
|
exit 1
|
|
fi
|
|
|
|
is_git_repo() {
|
|
if [[ -d "$1/.git" ]]; then return 0; fi
|
|
if [[ -f "$1/.git" ]]; then
|
|
gitdir=$(grep -m1 "gitdir:" "$1/.git" | cut -d' ' -f2 | tr -d ' ')
|
|
[[ -n "$gitdir" ]]; return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
if ! is_git_repo "$REPO_PATH"; then
|
|
echo "Not a git repo: $REPO_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
USER="$(secret-mgr get-username --key git)"
|
|
PASS="$(secret-mgr get-secret --key git)"
|
|
ENC_USER="$(U="$USER" python3 - <<'PY'
|
|
import os, urllib.parse
|
|
print(urllib.parse.quote(os.environ['U'], safe=''))
|
|
PY
|
|
)"
|
|
|
|
configure_repo() {
|
|
local repo="$1" relative="${2:-}"
|
|
local name="${relative:-$repo}"
|
|
echo "Configuring: $name"
|
|
( cd "$repo" && git config user.name "$USER" )
|
|
( cd "$repo" && git config user.email "$EMAIL" )
|
|
local git_dir
|
|
git_dir="$(cd "$repo" && git rev-parse --absolute-git-dir)"
|
|
local cred_file="${git_dir}/credentials"
|
|
( cd "$repo" && git config credential.helper "store --file ${cred_file}" )
|
|
( cd "$repo" && GIT_ASKPASS=true git credential-store --file "${cred_file}" store <<EOF
|
|
protocol=https
|
|
host=git.hangman-lab.top
|
|
username=${ENC_USER}
|
|
password=${PASS}
|
|
EOF
|
|
)
|
|
}
|
|
|
|
configure_repo "$REPO_PATH"
|
|
|
|
if [[ "$RECURSIVE" == "true" ]]; then
|
|
submodules=$(cd "$REPO_PATH" && git submodule status --recursive 2>/dev/null | awk '{print $2}' || true)
|
|
for sm in $submodules; do
|
|
sm_path="$REPO_PATH/$sm"
|
|
if is_git_repo "$sm_path"; then
|
|
configure_repo "$sm_path" "$sm"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
echo "OK"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────
|
|
# get-latest
|
|
# ─────────────────────────────────────────────
|
|
do_get_latest() {
|
|
local REPO_NAME="" BRANCH="main" RECURSIVE=false FORCE=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--recursive) RECURSIVE=true; shift ;;
|
|
--force) FORCE=true; shift ;;
|
|
-*)
|
|
echo "Unknown option: $1"
|
|
echo "Usage: $0 get-latest <repo-name> [branch] [--recursive] [--force]"
|
|
exit 1
|
|
;;
|
|
*)
|
|
[[ -z "$REPO_NAME" ]] && REPO_NAME="$1" && shift && continue
|
|
[[ -z "$BRANCH" ]] && BRANCH="$1" && shift && continue
|
|
echo "Unknown argument: $1"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$REPO_NAME" ]]; then
|
|
echo "Usage: $0 get-latest <repo-name> [branch] [--recursive] [--force]"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
|
echo "Error: script must be executed by pcexec"
|
|
exit 1
|
|
fi
|
|
|
|
local REPO_DIR="${AGENT_WORKSPACE}/${REPO_NAME}"
|
|
|
|
if [[ -d "$REPO_DIR" ]]; then
|
|
# Repo exists locally — update in place
|
|
if ! git -C "$REPO_DIR" rev-parse --is-inside-work-tree 2>/dev/null; then
|
|
echo "Error: $REPO_DIR is not a git repository"
|
|
exit 1
|
|
fi
|
|
|
|
local dirty
|
|
dirty=$(git -C "$REPO_DIR" status --porcelain 2>/dev/null)
|
|
if [[ -n "$dirty" ]] && [[ "$FORCE" != "true" ]]; then
|
|
echo "Error: $REPO_DIR has uncommitted changes. Use --force to discard them."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "$dirty" ]]; then
|
|
echo "Discarding uncommitted changes..."
|
|
git -C "$REPO_DIR" checkout -- . 2>/dev/null || true
|
|
fi
|
|
|
|
echo "Fetching: $REPO_NAME ($BRANCH)"
|
|
git -C "$REPO_DIR" fetch origin "$BRANCH" 2>/dev/null || true
|
|
git -C "$REPO_DIR" checkout "$BRANCH" 2>/dev/null || git -C "$REPO_DIR" checkout -b "$BRANCH" "origin/$BRANCH" 2>/dev/null || true
|
|
|
|
if [[ "$RECURSIVE" == "true" ]]; then
|
|
echo "Updating submodules..."
|
|
git -C "$REPO_DIR" submodule update --init --recursive 2>/dev/null || true
|
|
fi
|
|
|
|
echo "Done: $REPO_DIR updated."
|
|
else
|
|
# Repo does not exist locally — search via API then clone
|
|
echo "Repo not found locally. Searching..."
|
|
local search_result=""
|
|
search_result=$(bash "$SCRIPT_DIR/repo" search "$REPO_NAME" 2>&1)
|
|
local search_exit=$?
|
|
|
|
if [[ $search_exit -ne 0 ]]; then
|
|
echo "Error: repository '$REPO_NAME' not found"
|
|
exit 1
|
|
fi
|
|
|
|
local url=""
|
|
url=$(echo "$search_result" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
print(data.get('clone-url', ''))
|
|
" 2>/dev/null)
|
|
|
|
if [[ -z "$url" ]]; then
|
|
echo "Error: failed to parse clone URL for '$REPO_NAME'"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Cloning: $url"
|
|
git clone "$url" "$REPO_DIR" 2>/dev/null || {
|
|
echo "Error: git clone failed"
|
|
exit 1
|
|
}
|
|
|
|
do_config --repo-path "$REPO_DIR"
|
|
|
|
if [[ "$RECURSIVE" == "true" ]]; then
|
|
echo "Updating submodules..."
|
|
git -C "$REPO_DIR" submodule update --init --recursive 2>/dev/null || true
|
|
fi
|
|
|
|
echo "Done: $REPO_NAME cloned to $REPO_DIR."
|
|
fi
|
|
}
|
|
|
|
# ─────────────────────────────────────────────
|
|
# search
|
|
# ─────────────────────────────────────────────
|
|
do_search() {
|
|
if [[ $# -lt 1 ]]; then
|
|
echo "Usage: $0 search <exact-repo-name>"
|
|
exit 1
|
|
fi
|
|
|
|
REPO_NAME="$1"
|
|
|
|
TOKEN=$(secret-mgr get-secret --key git-access-token 2>/dev/null || secret-mgr get-secret --key git)
|
|
|
|
RESP=$(curl -s -H "Authorization: token $TOKEN" \
|
|
"https://git.hangman-lab.top/api/v1/repos/search?q=${REPO_NAME}")
|
|
|
|
RESULT=$(echo "$RESP" | python3 -c "
|
|
import sys, json
|
|
data = json.load(sys.stdin)
|
|
results = data.get('data', []) if isinstance(data, dict) else []
|
|
for r in results:
|
|
if isinstance(r, dict) and r.get('name') == '${REPO_NAME}':
|
|
owner = r.get('owner', {})
|
|
login = owner.get('login', '') if isinstance(owner, dict) else ''
|
|
out = {
|
|
'name': r.get('name', ''),
|
|
'id': r.get('id'),
|
|
'owner': login,
|
|
'clone-url': r.get('clone_url', '')
|
|
}
|
|
print(json.dumps(out))
|
|
sys.exit(0)
|
|
print('NOT_FOUND')
|
|
" 2>/dev/null)
|
|
|
|
if [[ "$RESULT" == "NOT_FOUND" ]]; then
|
|
echo "Error: repository '$REPO_NAME' not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "$RESULT"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────
|
|
# Dispatch
|
|
# ─────────────────────────────────────────────
|
|
if [[ $# -lt 1 ]]; then
|
|
echo "Usage: $0 <create|add-collaborators|list-all|config|search> [args...]"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " create <repo-name> Create a new repository"
|
|
echo " add-collaborators --user <u> --repo <r> Add collaborator"
|
|
echo " list-all List all visible repositories"
|
|
echo " config --repo-path <path> [--recursive] Configure repo credentials"
|
|
echo " get-latest <repo-name> [branch] [--recursive] [--force] Pull latest or clone if missing"
|
|
echo " search <exact-repo-name> Search for a repository by exact name"
|
|
exit 1
|
|
fi
|
|
|
|
subcommand="$1"; shift
|
|
|
|
case "$subcommand" in
|
|
create) do_create "$@" ;;
|
|
add-collaborators) do_add_collaborators "$@" ;;
|
|
list-all) do_list_all "$@" ;;
|
|
config) do_config "$@" ;;
|
|
get-latest) do_get_latest "$@" ;;
|
|
search) do_search "$@" ;;
|
|
*) echo "Unknown command: $subcommand"; exit 1 ;;
|
|
esac |