Skip to content

Instantly share code, notes, and snippets.

@dzogrim
Created March 20, 2025 17:24
Show Gist options
  • Save dzogrim/695fce63006ad8fdf7b79e76a9f12d1b to your computer and use it in GitHub Desktop.
Save dzogrim/695fce63006ad8fdf7b79e76a9f12d1b to your computer and use it in GitHub Desktop.
This script migrates repositories from a source Gitea instance to another Gitea instance
#!/usr/bin/env bash
# This script migrates repositories from a source Gitea instance to another Gitea instance.
# It clones all personal repositories from the source using HTTPS and pushes them to the
# destination using SSH, with an option for a default dry-run mode.
#
# USAGE:
# ./gitea-migr.sh # Runs in dry-run mode (NO actual cloning or pushing)
# ./gitea-migr.sh --no-dry-run # Performs the actual migration (cloning and pushing)
# πŸ”Ή Configuration
GITEA_SOURCE="https://"
TOKEN_SOURCE=""
USER_SOURCE=""
DEST_DOMAIN="git.toto.com"
GITEA_DEST="https://$DEST_DOMAIN"
GITEA_DEST_SSH="gitea@$DEST_DOMAIN"
TOKEN_DEST=""
USER_DEST=""
# Get the ID with:
# > curl --insecure -H 'Authorization: token $TOKEN_DEST' "$GITEA_DEST/api/v1/user" | jq '.id'
UID_DEST="55"
ERRORS=0
BACKUP_DIR="./gitea_migration"
DRY_RUN=true
# πŸ”Ή Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# πŸ”Ή Parse arguments
for arg in "$@"; do
if [[ "$arg" == "--no-dry-run" ]]; then
DRY_RUN=false
fi
done
# πŸ”Ή Create backup directory
mkdir -p "$BACKUP_DIR"
# πŸ”Ή Fetch all repositories from source Gitea
repos=$(curl --insecure -s -H "Authorization: token $TOKEN_SOURCE" \
"$GITEA_SOURCE/api/v1/user/repos?visibility=all&type=all&limit=300" | \
jq -r '.[] | select(.owner.login=="'"$USER_SOURCE"'") | .clone_url')
for repo in $repos; do
repo_name=$(basename "$repo" .git)
repo_path="$BACKUP_DIR/$repo_name.git"
# βœ… Step 1: Clone repository from source Gitea (using HTTPS)
if [[ -d "$repo_path" ]]; then
echo -e "${YELLOW}πŸ”Ή $repo_name already cloned, skipping...${NC}"
else
echo -e "${GREEN}⬇️ Cloning $repo_name from $GITEA_SOURCE (HTTPS)...${NC}"
if [[ "$DRY_RUN" == "false" ]]; then
git clone --mirror "$repo" "$repo_path"
if [[ $? -ne 0 ]]; then
echo -e "${RED}❌ Failed to clone $repo_name.${NC}"
ERRORS=$((ERRORS+1))
continue
fi
else
echo -e " πŸ›‘ ${YELLOW}Dry-run: skipping clone.${NC}"
fi
fi
done
echo -e "${GREEN}βœ… All repositories downloaded.${NC}"
# πŸ”Ή Push repositories to destination Gitea
for repo_path in "$BACKUP_DIR"/*.git; do
repo_name=$(basename "$repo_path" .git)
# βœ… Step 2: Check if repository exists on destination (handling ghost repositories)
repo_check=$(curl --insecure -s -H "Authorization: token $TOKEN_DEST" \
"$GITEA_DEST/api/v1/repos/$USER_DEST/$repo_name")
if echo "$repo_check" | jq -e '.message == "Not Found"' >/dev/null; then
echo -e "${GREEN}πŸ†• Repository does not exist, creating it...${NC}"
if [[ "$DRY_RUN" == "false" ]]; then
curl -X POST -H "Content-Type: application/json" \
-H "Authorization: token $TOKEN_DEST" \
-d '{
"name": "'"$repo_name"'",
"private": true,
"uid": '"$UID_DEST"'
}' "$GITEA_DEST/api/v1/user/repos"
if [[ $? -ne 0 ]]; then
echo -e "${RED}❌ Failed to create repository $repo_name.${NC}"
ERRORS=$((ERRORS+1))
continue
fi
else
echo -e " πŸ›‘ ${YELLOW}Dry-run: skipping repository creation.${NC}"
fi
else
echo -e "${YELLOW}🚫 Repository $repo_name already exists on destination, skipping creation but still trying to push.${NC}"
fi
# βœ… Step 3: Check if the repository exists locally before pushing
if [[ ! -d "$repo_path" ]]; then
echo -e "${RED}❌ Error: Local repository folder '$repo_path' does not exist. Skipping push.${NC}"
ERRORS=$((ERRORS+1))
continue
fi
# βœ… Step 4: Push repository to destination (using SSH)
echo -e "${GREEN}⬆️ Pushing $repo_name to $GITEA_DEST_SSH...${NC}"
if [[ "$DRY_RUN" == "false" ]]; then
git -C "$repo_path" remote set-url origin "$GITEA_DEST_SSH:$USER_DEST/$repo_name.git"
# Push and handle errors
push_output=$(git -C "$repo_path" push --mirror 2>&1)
if echo "$push_output" | grep -q "Gitea: Push to create is not enabled"; then
echo -e "${RED}❌ Failed to push $repo_name: Push to create is not enabled.${NC}"
ERRORS=$((ERRORS+1))
continue
elif echo "$push_output" | grep -q "fatal"; then
echo -e "${RED}❌ Failed to push $repo_name: $push_output${NC}"
ERRORS=$((ERRORS+1))
continue
fi
else
echo -e " πŸ›‘ ${YELLOW}Dry-run: skipping push.${NC}"
fi
echo -e "${GREEN}βœ… $repo_name migration completed.${NC}"
done
# πŸ”Ή Final Message
if [[ "$DRY_RUN" == "true" ]]; then
echo -e "${YELLOW}⚠️ Dry-run mode: No actual changes were made.${NC}"
echo -e "${YELLOW} To perform the migration, run: ./gitea-migr.sh --no-dry-run${NC}"
elif [[ "$ERRORS" -gt 0 ]]; then
echo -e "${RED}⚠️ Migration completed with $ERRORS errors.${NC}"
exit 1
else
echo -e "${GREEN}πŸŽ‰ Migration complete! All repositories have been successfully migrated.${NC}"
exit 0
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment