Forked from franklioxygen/mirror_github_starred_repos.sh
Last active
April 17, 2025 22:26
-
-
Save fnuecke/f31c955cccdbc0889cf282369c92ac5a to your computer and use it in GitHub Desktop.
This script automatically mirrors GitHub starred repositories to a Gitea instance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
######################################################################### | |
# GitHub to Gitea Starred Repository Mirror | |
# | |
# This script automatically mirrors your GitHub starred repositories to | |
# a Gitea instance. | |
# | |
# - Fetches all repositories starred on GitHub | |
# - Recreate organization / owner (as org) to mirror repos into | |
# - Creates mirror repositories on Gitea for each starred GitHub repo | |
# - Handles pagination for users with many starred repositories | |
# | |
# Requirements: | |
# - GitHub Personal Access Token with permissions to read starred repos | |
# - Gitea Access Token with organization and repository write access | |
# - curl, jq installed on your system | |
######################################################################### | |
# Read variables from .env file. | |
if ! [ -f ".env" ]; then | |
echo "No .env file found to source variables from." | |
echo "Please create a .env file with the variables:" | |
echo 'GITHUB_USER="jane_doe"' | |
echo 'GITHUB_TOKEN="github_pat_..."' | |
echo 'GITEA_URL="https://gitea.example.com"' | |
echo 'GITEA_USER="jane_doe"' | |
echo 'GITEA_TOKEN="..."' | |
exit 1 | |
fi | |
source '.env' | |
# Make sure all variables are set. | |
required_variables=("GITHUB_USER" "GITHUB_TOKEN" "GITEA_URL" "GITEA_USER" "GITEA_TOKEN") | |
for variable_name in "${required_variables[@]}"; do | |
if ! [ -v $variable_name ]; then | |
echo "$variable_name is not set." | |
exit 1 | |
fi | |
done | |
# $1: method (e.g. POST) | |
# $2: endpoint (e.g. users/some_user/starred?page=1) | |
# $3: data (e.g. '{"key": "value"}') | |
function github_query() { | |
method=$1 | |
endpoint=$2 | |
if [ -v 3 ]; then | |
data=$3 | |
curl -s -X "$method" -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json" -d "$data" "https://api.github.com/$endpoint" | |
else | |
curl -s -X "$method" -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/$endpoint" | |
fi | |
} | |
function gitea_query() { | |
method=$1 | |
endpoint=$2 | |
if [ -v 3 ]; then | |
data=$3 | |
curl -s -X "$method" -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" -d "$data" "$GITEA_URL/api/v1/$endpoint" | |
else | |
curl -s -X "$method" -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" "$GITEA_URL/api/v1/$endpoint" | |
fi | |
} | |
echo "Fetching Gitea organisations..." | |
page=1 | |
existing_orgs=() | |
while true; do | |
orgs_page=$(gitea_query "GET" "orgs?page=$page") | |
if [ "$(echo "$orgs_page" | jq '. | length')" = "0" ]; then | |
break | |
fi | |
page=$((page + 1)) | |
while read -r org; do | |
name=$(echo "$org" | jq -r '.name') | |
existing_orgs+=("$name") | |
done < <(echo "$orgs_page" | jq -c '.[]') | |
done | |
echo "Found ${#existing_orgs[@]} organisations on Gitea." | |
# Flatten for regex-check below. | |
existing_orgs=$(printf "%s\n" "${existing_orgs[@]}") | |
echo "Fetching Gitea repositories..." | |
page=1 | |
existing_repos=() | |
while true; do | |
repos_page=$(gitea_query "GET" "repos/search?page=$page") | |
if [ "$(echo "$repos_page" | jq '.data | length')" = "0" ]; then | |
break | |
fi | |
page=$((page + 1)) | |
while read -r repo; do | |
name=$(echo "$repo" | jq -r '.full_name') | |
existing_repos+=("$name") | |
done < <(echo "$repos_page" | jq -c '.data[]') | |
done | |
echo "Found ${#existing_repos[@]} repositories on Gitea." | |
# Flatten for regex-check below. | |
existing_repos=$(printf "%s\n" "${existing_repos[@]}") | |
echo "Fetching starred repositories from GitHub..." | |
page=1 | |
while true; do | |
starred_page=$(github_query "GET" "users/$GITHUB_USER/starred?per_page=100&page=$page") | |
if [ "$(echo "$starred_page" | jq '. | length')" = "0" ]; then | |
break | |
fi | |
page=$((page + 1)) | |
while read -r repo; do | |
# Read values in one query for efficiency, needs readarray because of multi-line output. | |
readarray -t repo_info < <(echo "$repo" | jq -r '.name, .clone_url, .description // "", .owner.login') | |
name=${repo_info[0]} | |
url=${repo_info[1]} | |
description=${repo_info[2]} | |
owner=${repo_info[3]} | |
# Skip if repo already exists in Gitea. | |
if echo "$existing_repos" | grep -q "^$owner/$name$"; then | |
continue | |
fi | |
echo "Found new repository to mirror: $owner/$name..." | |
if ! echo "$existing_orgs" | grep -q "^$owner$"; then | |
echo "Creating new organisation: $owner..." | |
query=$(jq -n --arg name "$owner" '{"username": $name, "website": "https://github.com/\($name)"}') | |
result=$(gitea_query "POST" "orgs" "$query") | |
id=$(echo "$result" | jq -r '.id') | |
if [ -z "$id" ] || [ "$id" = "null" ]; then | |
echo "Error creating Gitea organisation: $result" | |
continue | |
fi | |
fi | |
query=$(jq -n --arg name "$name" --arg url "$url" --arg description "$description" --arg owner "$owner" --arg user "$GITHUB_USER" --arg token "$GITHUB_TOKEN" '{ | |
"clone_addr": $url, | |
"repo_owner": $owner, | |
"repo_name": $name, | |
"description": $description, | |
"mirror": true, | |
"private": false, | |
"lfs": true, | |
"auth_username": $user, | |
"auth_token": $token | |
}') | |
result=$(gitea_query "POST" "repos/migrate" "$query") | |
gitea_repo_url=$(echo "$result" | jq -r '.clone_url') | |
if [ -z "$gitea_repo_url" ] || [ "$gitea_repo_url" = "null" ]; then | |
echo "Error creating mirror: $result" | |
continue | |
fi | |
echo "Successfully set up mirror for $owner/$name." | |
done < <(echo "$starred_page" | jq -c '.[]') | |
done | |
echo "Mirroring process completed!" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment