#!/bin/bash # # Bulk-migrate every GitLab project into Forgejo with full data # (code, issues, merge requests, labels, milestones, releases, wiki). # # Runs both forges in parallel — GitLab is only read, never modified. # # Usage: # export GITLAB_URL=https://gitlab.nasarek.dev # export GITLAB_TOKEN=glpat-xxxxxxxx # scopes: read_api, read_repository # export FORGEJO_URL=https://git.nasarek.dev # export FORGEJO_TOKEN=xxxxxxxx # Forgejo app token, repo + org scope # export FORGEJO_OWNER=root # target user/org that will own the repos # ./migrate-from-gitlab.sh [--dry-run] # set -euo pipefail DRY_RUN=false [ "${1:-}" = "--dry-run" ] && DRY_RUN=true : "${GITLAB_URL:?set GITLAB_URL}" : "${GITLAB_TOKEN:?set GITLAB_TOKEN}" : "${FORGEJO_URL:?set FORGEJO_URL}" : "${FORGEJO_TOKEN:?set FORGEJO_TOKEN}" : "${FORGEJO_OWNER:?set FORGEJO_OWNER (target user/org)}" command -v jq >/dev/null || { echo "jq is required"; exit 1; } # Resolve the Forgejo owner's numeric uid (repos/migrate needs repo_owner name + uid). OWNER_UID=$(curl -fsS -H "Authorization: token ${FORGEJO_TOKEN}" \ "${FORGEJO_URL}/api/v1/users/${FORGEJO_OWNER}" | jq -r '.id') echo "Target owner: ${FORGEJO_OWNER} (uid=${OWNER_UID})" migrate_one() { local clone_url="$1" name="$2" private="$3" desc="$4" echo "==> ${name} (private=${private})" if $DRY_RUN; then return 0; fi local payload payload=$(jq -n \ --arg addr "$clone_url" --arg token "$GITLAB_TOKEN" \ --arg name "$name" --arg owner "$FORGEJO_OWNER" \ --argjson uid "$OWNER_UID" --argjson private "$private" \ --arg desc "$desc" \ '{clone_addr:$addr, service:"gitlab", auth_token:$token, repo_name:$name, repo_owner:$owner, uid:$uid, private:$private, description:$desc, issues:true, pull_requests:true, labels:true, milestones:true, releases:true, wiki:true}') local code code=$(curl -s -o /tmp/forgejo_migrate_resp.json -w '%{http_code}' \ -H "Authorization: token ${FORGEJO_TOKEN}" -H "Content-Type: application/json" \ -X POST "${FORGEJO_URL}/api/v1/repos/migrate" -d "$payload") if [ "$code" = "201" ]; then echo " OK" elif [ "$code" = "409" ]; then echo " SKIP (already exists)" else echo " FAILED (HTTP $code): $(jq -r '.message // .' /tmp/forgejo_migrate_resp.json)" fi } # Page through all GitLab projects the token can see. page=1 while :; do resp=$(curl -fsS -H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \ "${GITLAB_URL}/api/v4/projects?membership=true&per_page=100&page=${page}&simple=false") count=$(echo "$resp" | jq 'length') [ "$count" -eq 0 ] && break while IFS=$'\t' read -r http_url repo_path visibility description; do private=true; [ "$visibility" = "public" ] && private=false # Use the bare repo slug (e.g. "dfg_3dviewer_embed"), dropping the GitLab namespace. migrate_one "$http_url" "$repo_path" "$private" "${description:-}" done < <(echo "$resp" | jq -r '.[] | [.http_url_to_repo, .path, .visibility, (.description // "")] | @tsv') page=$((page + 1)) done echo "Done."