forgejo-batch-migrate
Batch-migrate repositories from GitHub, GitLab or another Forgejo/Gitea instance into a target Forgejo instance.
The tool wraps Forgejo's own migration API (POST /api/v1/repos/migrate) and exposes every option the web migration assistant offers.
It can migrate a whole source organization/group or user in one go, or a hand-picked list of repositories, and it makes conservative rate-limiting assumptions so large batches do not overwhelm either forge.
Features
- Sources: GitHub, GitLab, Forgejo, Gitea, and plain
git clone URLs.
- Migrate an entire org/group (
--source-org), a whole user (--source-user), or individual repos (--source-repo, repeatable).
- All assistant options: issues, pull requests, labels, milestones, releases, wiki, LFS, mirror, private, and mirror interval.
- Content options default to on; pass
--issues=false (and friends) to skip.
- Private source repositories are always created private on the target, regardless of the
--private default.
- Graceful degradation: options are automatically skipped for repositories whose source has that unit disabled (e.g. requesting releases when the source repo has releases turned off), so one disabled unit does not fail the whole migration.
- Smart rate limiting: a global token bucket, a bounded worker pool, and automatic backoff on HTTP 429 (honoring
Retry-After and GitHub's X-RateLimit-*).
- One-shot flags for quick runs, or a YAML config describing many jobs for batch/cron use.
--dry-run resolves and prints the full plan without writing anything.
Install
Install script (recommended)
The install script detects your OS and architecture, downloads and verifies the matching archive from the latest release, extracts the binary into a local bin directory (~/.local/bin by default), and creates an fbm alias next to it:
curl -fsSL https://codefloe.com/codefloe/forgejo-batch-migrate/raw/branch/main/install.sh | sh
Override the defaults with environment variables:
# pin a specific version
curl -fsSL https://codefloe.com/codefloe/forgejo-batch-migrate/raw/branch/main/install.sh | VERSION=v0.1.0 sh
# install somewhere else
curl -fsSL https://codefloe.com/codefloe/forgejo-batch-migrate/raw/branch/main/install.sh | INSTALL_DIR=/usr/local/bin sh
# skip creating the `fbm` alias
curl -fsSL https://codefloe.com/codefloe/forgejo-batch-migrate/raw/branch/main/install.sh | NO_ALIAS=1 sh
If the install directory is not on your PATH, the script prints the line to add.
Manual download
Download a pre-built archive from the latest release.
Archives are published for Linux, macOS, and Windows (amd64 and arm64) as .tar.gz (Unix) and .zip (Windows), alongside a SHA256SUMS.txt for verification:
# Linux (amd64)
curl -LO https://codefloe.com/codefloe/forgejo-batch-migrate/releases/download/v0.1.0/forgejo-batch-migrate-linux-amd64.tar.gz
# Linux (arm64)
# curl -LO https://codefloe.com/codefloe/forgejo-batch-migrate/releases/download/v0.1.0/forgejo-batch-migrate-linux-arm64.tar.gz
# macOS (Intel)
# curl -LO https://codefloe.com/codefloe/forgejo-batch-migrate/releases/download/v0.1.0/forgejo-batch-migrate-darwin-amd64.tar.gz
# macOS (Apple Silicon)
# curl -LO https://codefloe.com/codefloe/forgejo-batch-migrate/releases/download/v0.1.0/forgejo-batch-migrate-darwin-arm64.tar.gz
tar -xzf forgejo-batch-migrate-linux-amd64.tar.gz
chmod +x forgejo-batch-migrate
# Windows (amd64): download the .zip and extract it
# curl -LO https://codefloe.com/codefloe/forgejo-batch-migrate/releases/download/v0.1.0/forgejo-batch-migrate-windows-amd64.zip
Or install with the Go toolchain:
go install codefloe.com/codefloe/forgejo-batch-migrate/cmd@latest
Build from source
Build from source with just:
just build
# binary at build/forgejo-batch-migrate
Or with the Go toolchain directly:
go build -o forgejo-batch-migrate ./cmd
Authentication
Provide a token for both forges. In order of preference:
- Environment variables
SOURCE_TOKEN / FORGEJO_TARGET_TOKEN (recommended).
- Token files:
--source-token-file / --target-token-file (e.g. a mounted secret).
- Literal flags
--source-token / --target-token (discouraged: the value is visible in the process list and shell history, so the tool warns when you use them).
- The target token is required for any real migration (a run without it fails fast;
--dry-run is exempt).
- A source token is strongly recommended even for public repositories: unauthenticated API access is rate-limited aggressively (e.g. GitHub allows only 60 requests/hour), so listing or migrating more than a few repos will otherwise stall. The tool warns when a source token is missing.
Usage
Migrate a whole GitHub organization
export SOURCE_TOKEN=ghp_...
export FORGEJO_TARGET_TOKEN=...
forgejo-batch-migrate \
--source-type github \
--source-org acme \
--target-url https://codefloe.com \
--target-owner acme-mirror \
--create-org
Migrate a whole GitLab group (self-hosted)
Groups are addressed by their full path; subgroups are included automatically.
export SOURCE_TOKEN=glpat-...
forgejo-batch-migrate \
--source-type gitlab \
--source-url https://gitlab.example.com \
--source-org platform/backend \
--target-url https://codefloe.com \
--target-owner backend \
--create-org
Migrate a whole Forgejo organization
export SOURCE_TOKEN=...
export FORGEJO_TARGET_TOKEN=...
forgejo-batch-migrate \
--source-type forgejo \
--source-url https://codeberg.org \
--source-org acme \
--target-url https://codefloe.com \
--target-owner acme \
--create-org
Mirror selected repos from a single user
Pick individual repositories owned by one user with --source-repo, and use --mirror to keep the destination in sync with the source instead of a one-time copy.
forgejo-batch-migrate \
--source-type forgejo \
--source-url https://codefloe.com \
--source-repo alice/dotfiles \
--source-repo alice/notes \
--target-url https://codefloe.com \
--target-owner alice \
--mirror --mirror-interval 24h0m0s
Migrate hand-picked repositories, code only
Pass --source-repo multiple times and switch off the metadata you do not want.
forgejo-batch-migrate \
--source-type github \
--source-repo acme/api \
--source-repo acme/web \
--target-url https://codefloe.com \
--target-owner mirrors \
--issues=false --pull-requests=false --wiki=false
Migrate arbitrary Git URLs
--source-type git mirrors any plain Git repository; pass full clone URLs.
forgejo-batch-migrate \
--source-type git \
--source-repo https://git.example.com/team/tool.git \
--target-url https://codefloe.com \
--target-owner mirrors
Preview without migrating
Add --dry-run to any invocation to list the resolved repositories and per-repo options without contacting the target for writes.
This is the safe way to check what a whole-org migration would do before running it.
forgejo-batch-migrate \
--source-type github --source-org acme \
--target-url https://codefloe.com --target-owner mirrors \
--dry-run
Options
| Flag |
Config key |
Default |
Meaning |
--issues |
issues |
true |
Migrate issues. |
--pull-requests |
pull_requests |
true |
Migrate pull requests. |
--labels |
labels |
true |
Migrate labels. |
--milestones |
milestones |
true |
Migrate milestones. |
--releases |
releases |
true |
Migrate releases. |
--wiki |
wiki |
true |
Migrate the wiki. |
--lfs |
lfs |
false |
Migrate Git LFS objects. |
--mirror |
mirror |
false |
Set up the repo as a pull mirror. |
--private |
private |
false |
Force the destination repo to be private. |
--mirror-interval |
mirror_interval |
8h0m0s |
Mirror sync interval. |
--concurrency |
run.concurrency |
2 |
Concurrent migrations. |
--rate-limit |
run.rate_limit |
30 |
Max API requests per minute. |
--skip-existing |
run.skip_existing |
true |
Skip repos that already exist on the target. |
Development
just vendor # download + vendor + tidy modules
just fmt # prettier + gofmt + gci
just lint # golangci-lint
just test # go test ./...
just build # build the binary
Pre-commit hooks mirror the linters; run them with prek run -a.
License
AGPL-3.0