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
Note: Pre-built binaries are not published yet.
Build from source for now; this section will be replaced with download instructions once tagged releases are available.
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
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
Mirror every repo of another Forgejo/Gitea user
Use --mirror to keep the destination in sync with the source instead of a one-time copy.
forgejo-batch-migrate \
--source-type gitea \
--source-url https://gitea.example.com \
--source-user alice \
--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
Batch config
For repeatable runs, describe the work in a YAML file and pass --config:
forgejo-batch-migrate --config examples/nightly.yaml
See examples/nightly.yaml.
Precedence is: built-in defaults < config defaults < per-job fields < explicitly-set CLI flags.
target:
url: https://codefloe.com
owner: mirrors
create_org: true
run:
concurrency: 2
rate_limit: 30
skip_existing: true
defaults:
issues: true
pull_requests: true
jobs:
- source: { type: github, org: acme }
- source: { type: gitlab, url: https://gitlab.com, repos: [group/proj] }
private: true
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. |
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.
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