232 lines
7.3 KiB
Bash
232 lines
7.3 KiB
Bash
#!/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:<token>@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
|