Last active
March 30, 2026 17:14
-
-
Save mrcgrtz/52b8eeb304e1a030855a07e3c2c5cd56 to your computer and use it in GitHub Desktop.
Migrate from Bitbucket to Codeberg
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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ============================================================================= | |
| # migrate-bitbucket-to-codeberg.sh | |
| # | |
| # Batch-migrates all private Bitbucket repositories to Codeberg. | |
| # - Repos are created as private on Codeberg | |
| # - The default branch is renamed from `master` to `main` (if applicable) | |
| # - Local clones are cleaned up immediately after each push | |
| # - A temporary working directory is used and removed on exit | |
| # | |
| # Prerequisites: | |
| # - `git`, `curl`, and `jq` must be installed | |
| # - Bitbucket API Token with "Repository: Read" scope | |
| # (create at bitbucket.org → Personal Bitbucket settings → API tokens) | |
| # - Bitbucket account email address | |
| # (find at bitbucket.org → Personal Bitbucket settings → Email aliases) | |
| # - Codeberg API Token with "user" (read), "repository" (read + write) | |
| # and "issue" (read + write) permissions | |
| # (create at codeberg.org → Settings → Applications → Access Tokens) | |
| # | |
| # Usage: | |
| # chmod +x migrate-bitbucket-to-codeberg.sh | |
| # ./migrate-bitbucket-to-codeberg.sh | |
| # ============================================================================= | |
| # === Configuration === | |
| BITBUCKET_USER="..." # Your Bitbucket username (not email) | |
| BITBUCKET_EMAIL="..." # Your Atlassian account email (for REST API auth) | |
| BITBUCKET_API_TOKEN="..." # Your Bitbucket API token | |
| CODEBERG_USER="..." | |
| CODEBERG_TOKEN="..." | |
| # === Temporary working directory === | |
| # All bare clones are created here and cleaned up after each repo. | |
| # The trap ensures the directory is removed on exit, even on error. | |
| WORKDIR=$(mktemp -d) | |
| cd "$WORKDIR" | |
| trap "rm -rf '$WORKDIR'" EXIT | |
| # === Fetch all Bitbucket repos === | |
| echo "Fetching all Bitbucket repos for user: $BITBUCKET_USER ..." | |
| # REST API authentication uses email + API token (Basic Auth). | |
| # Note: If you have more than 100 repos, pagination needs to be implemented. | |
| repos=$(curl -s -u "$BITBUCKET_EMAIL:$BITBUCKET_API_TOKEN" \ | |
| "https://api.bitbucket.org/2.0/repositories/$BITBUCKET_USER?pagelen=100" \ | |
| | jq -r '.values[].name') | |
| echo "Found repos:" | |
| echo "$repos" | |
| # === For each repo: create on Codeberg, mirror, rename branch, push === | |
| for repo in $repos; do | |
| echo "--------------------------------------------" | |
| echo "Processing repo: $repo" | |
| # Create the repo on Codeberg as private. | |
| # HTTP 201 = created, 409 = already exists (both are fine). | |
| response=$(curl -s -o /dev/null -w "%{http_code}" -X POST "https://codeberg.org/api/v1/user/repos" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: token $CODEBERG_TOKEN" \ | |
| -d "{\"name\":\"$repo\",\"private\":true}") | |
| if [[ "$response" == "201" ]]; then | |
| echo "Created new repo on Codeberg: $repo" | |
| elif [[ "$response" == "409" ]]; then | |
| echo "Repo already exists on Codeberg: $repo" | |
| else | |
| echo "Failed to create repo on Codeberg: HTTP $response — skipping." | |
| continue | |
| fi | |
| # Clone a bare mirror from Bitbucket. | |
| # Git authentication uses the static username x-bitbucket-api-token-auth + API token. | |
| echo "Cloning mirror from Bitbucket..." | |
| git clone --mirror \ | |
| "https://x-bitbucket-api-token-auth:$BITBUCKET_API_TOKEN@bitbucket.org/$BITBUCKET_USER/$repo.git" \ | |
| || { echo "Clone failed, skipping $repo."; continue; } | |
| cd "$repo.git" | |
| # Rename master to main if master exists. | |
| # git symbolic-ref updates HEAD so Codeberg picks up main as the default branch. | |
| if git show-ref --verify --quiet refs/heads/master; then | |
| echo "Renaming master → main..." | |
| git branch -m master main | |
| git symbolic-ref HEAD refs/heads/main | |
| echo "Renamed master to main." | |
| else | |
| echo "No master branch found, skipping rename." | |
| fi | |
| # Push all refs to Codeberg. | |
| echo "Pushing mirror to Codeberg..." | |
| git remote set-url origin \ | |
| "https://$CODEBERG_USER:$CODEBERG_TOKEN@codeberg.org/$CODEBERG_USER/$repo.git" | |
| git push --mirror | |
| cd .. | |
| # Remove the local bare clone immediately after pushing. | |
| echo "Cleaning up local clone..." | |
| rm -rf "$repo.git" | |
| echo "Done: $repo" | |
| done | |
| echo "============================================" | |
| echo "All repos migrated!" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment