#!/usr/bin/env bash set -euo pipefail # Массовая миграция репозиториев GitHub -> Gitea. # Переносит всю git-историю через mirror push. Может переносить LFS (опционально). # # Пуш в Gitea: PUSH_WITH_SSH=true — нужен SSH-ключ на ЭТОЙ машине (добавлен в Gitea). # На сервере без ключа — PUSH_WITH_SSH=false и GITEA_USERNAME + GITEA_TOKEN (HTTPS). # # Только выбранные репозитории (вместо полного списка с GitHub): # GITHUB_REPOS="owner/repo1,owner/repo2" # и/или # GITHUB_REPOS_FILE=/path/to/list.txt # по одному owner/repo на строку, # — комментарий : "${GITHUB_TOKEN:?Set GITHUB_TOKEN}" : "${GITHUB_OWNER:?Set GITHUB_OWNER (user or org on GitHub)}" : "${GITHUB_OWNER_TYPE:?Set GITHUB_OWNER_TYPE=user|org}" : "${GITEA_URL:?Set GITEA_URL, e.g. https://git.papatramp.ru}" : "${GITEA_TOKEN:?Set GITEA_TOKEN (Gitea PAT)}" : "${GITEA_OWNER:?Set GITEA_OWNER (target user/org in Gitea)}" : "${GITEA_OWNER_TYPE:?Set GITEA_OWNER_TYPE=user|org}" WORKDIR="${WORKDIR:-$PWD/migration-workdir}" PUSH_WITH_SSH="${PUSH_WITH_SSH:-true}" # true/false GITEA_SSH_HOST="${GITEA_SSH_HOST:-git.papatramp.ru}" GITEA_SSH_PORT="${GITEA_SSH_PORT:-2222}" MIGRATE_LFS="${MIGRATE_LFS:-false}" # true/false # См. шапку: если задано — мигрируются только эти репозитории (полный вид owner/name). GITHUB_REPOS="${GITHUB_REPOS:-}" GITHUB_REPOS_FILE="${GITHUB_REPOS_FILE:-}" mkdir -p "$WORKDIR" need_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "Missing command: $1"; exit 1; }; } need_cmd curl need_cmd jq need_cmd git if [[ "$MIGRATE_LFS" == "true" ]]; then need_cmd git-lfs fi gh_api() { local url="$1" curl -fsSL \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ "$url" } gitea_api() { local method="$1" local url="$2" local data="${3:-}" if [[ -n "$data" ]]; then curl -sS -o /tmp/gitea_resp.json -w "%{http_code}" \ -X "$method" \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ -d "$data" \ "$url" else curl -sS -o /tmp/gitea_resp.json -w "%{http_code}" \ -X "$method" \ -H "Authorization: token $GITEA_TOKEN" \ "$url" fi } create_gitea_repo_if_needed() { local repo_name="$1" local private="$2" local payload endpoint code payload="$(jq -n --arg name "$repo_name" --argjson private "$private" \ '{name:$name, private:$private, auto_init:false}')" if [[ "$GITEA_OWNER_TYPE" == "org" ]]; then endpoint="$GITEA_URL/api/v1/orgs/$GITEA_OWNER/repos" else endpoint="$GITEA_URL/api/v1/user/repos" fi code="$(gitea_api POST "$endpoint" "$payload")" if [[ "$code" == "201" ]]; then echo " [OK] Created: $GITEA_OWNER/$repo_name" elif [[ "$code" == "409" ]]; then echo " [SKIP] Exists: $GITEA_OWNER/$repo_name" else echo " [ERR] Create repo failed: HTTP $code" cat /tmp/gitea_resp.json return 1 fi } push_mirror() { local mirror_dir="$1" local repo_name="$2" local remote_url if [[ "$PUSH_WITH_SSH" == "true" ]]; then remote_url="ssh://git@${GITEA_SSH_HOST}:${GITEA_SSH_PORT}/${GITEA_OWNER}/${repo_name}.git" else : "${GITEA_USERNAME:?Set GITEA_USERNAME when PUSH_WITH_SSH=false}" remote_url="https://${GITEA_USERNAME}:${GITEA_TOKEN}@${GITEA_URL#https://}/${GITEA_OWNER}/${repo_name}.git" fi if git -C "$mirror_dir" remote | grep -q '^gitea$'; then git -C "$mirror_dir" remote set-url gitea "$remote_url" else git -C "$mirror_dir" remote add gitea "$remote_url" fi git -C "$mirror_dir" push --mirror gitea } migrate_repo() { local full_name="$1" local private="$2" local repo_name="${full_name##*/}" local mirror_dir="$WORKDIR/${repo_name}.git" echo "=== $full_name (private=$private) ===" rm -rf "$mirror_dir" # GitHub HTTPS + PAT: способ через Authorization header иногда ломается из‑за credential.helper / # старых версий Git. Способ oauth2:@github.com поддерживается GitHub для PAT и fine-grained. # credential.helper= отключает подмешивание сохранённых логинов к github.com. GIT_TERMINAL_PROMPT=0 git -c credential.helper= clone --mirror \ "https://oauth2:${GITHUB_TOKEN}@github.com/${full_name}.git" "$mirror_dir" create_gitea_repo_if_needed "$repo_name" "$private" push_mirror "$mirror_dir" "$repo_name" if [[ "$MIGRATE_LFS" == "true" ]]; then git -C "$mirror_dir" lfs fetch --all || true git -C "$mirror_dir" lfs push --all gitea || true fi echo " [DONE] $full_name" } migrate_repo_from_github_api() { local full_name="$1" local repo_json private repo_json="$(gh_api "https://api.github.com/repos/${full_name}")" if ! echo "$repo_json" | jq -e '.full_name' >/dev/null 2>&1; then echo "[ERR] Репозиторий недоступен или не найден на GitHub: ${full_name}" echo "$repo_json" | jq -r '.message // .' 2>/dev/null || echo "$repo_json" exit 1 fi private="$(echo "$repo_json" | jq -r '.private')" migrate_repo "$full_name" "$private" } migrate_selected_repos() { local names=() line part if [[ -n "$GITHUB_REPOS_FILE" ]]; then if [[ ! -f "$GITHUB_REPOS_FILE" ]]; then echo "[ERR] GITHUB_REPOS_FILE не найден: $GITHUB_REPOS_FILE" exit 1 fi while IFS= read -r line || [[ -n "$line" ]]; do line="${line%%#*}" line="$(echo "$line" | xargs)" [[ -z "$line" ]] && continue names+=("$line") done <"$GITHUB_REPOS_FILE" fi if [[ -n "$GITHUB_REPOS" ]]; then local -a repo_parts IFS=',' read -ra repo_parts <<<"${GITHUB_REPOS}" for part in "${repo_parts[@]}"; do part="$(echo "$part" | xargs)" [[ -z "$part" ]] && continue names+=("$part") done fi if [[ "${#names[@]}" -eq 0 ]]; then echo "[ERR] Указаны GITHUB_REPOS / GITHUB_REPOS_FILE, но список имён пуст." exit 1 fi echo "[INFO] Режим выборочной миграции, репозиториев: ${#names[@]}" local fn for fn in "${names[@]}"; do if [[ "$fn" != */* ]]; then echo "[ERR] Ожидается полное имя owner/repo, получено: $fn" exit 1 fi migrate_repo_from_github_api "$fn" done echo "Выбранные репозитории обработаны." } main() { if [[ -n "$GITHUB_REPOS" || -n "$GITHUB_REPOS_FILE" ]]; then migrate_selected_repos return 0 fi local page=1 local per_page=100 local url repos_json count while true; do if [[ "$GITHUB_OWNER_TYPE" == "org" ]]; then url="https://api.github.com/orgs/${GITHUB_OWNER}/repos?type=all&per_page=${per_page}&page=${page}" else url="https://api.github.com/users/${GITHUB_OWNER}/repos?type=owner&per_page=${per_page}&page=${page}" fi repos_json="$(gh_api "$url")" count="$(echo "$repos_json" | jq 'length')" [[ "$count" -eq 0 ]] && break while IFS=$'\t' read -r full_name private; do migrate_repo "$full_name" "$private" done < <(echo "$repos_json" | jq -r '.[] | [.full_name, .private] | @tsv') ((page++)) done echo "All repositories migrated." } main