Mirror all your GitHub repositories to GitLab automatically — private stays private, public stays public.
Having all your code on a single platform is a risk. This tool clones every repository from your GitHub account and creates matching repositories on GitLab, preserving visibility exactly. Run it once for a full backup, run it again anytime to sync incrementally.
- Visibility-safe — private repos are never created as public; defaults to private when in doubt
- Idempotent — safe to re-run; existing GitLab repos receive incremental pushes, not duplicates
- Full mirror — clones all branches and tags via
git clone --mirror - Dry-run mode — preview every action before executing a single write
- Selective backup — filter by glob pattern (
--filter "myproject-*") - Works on Windows — handles read-only
.gitfiles; tested on Windows 10 / PowerShell - Rich terminal output — progress bar and per-repo status table
- Python 3.11+
gitin your PATH- SSH key registered on both GitHub and GitLab
- GitHub token with
reposcope - GitLab token with
apiscope
git clone git@github.com:paladini/backup-github-to-gitlab.git
cd backup-github-to-gitlab
pip install -r requirements.txt
cp config.example.yaml config.yaml # then edit: set github.username and gitlab.username
cp .env.example .env # then edit: set GITHUB_TOKEN and GITLAB_TOKEN
python backup.py --dry-run # preview — no changes made
python backup.py # run the backup- Go to Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click Generate new token (classic)
- Select scope:
repo(required to list private repositories) - Paste the token into
.envasGITHUB_TOKEN
- Go to User Settings → Access Tokens → Add new token
- Select scope:
api(required to create projects) - Paste the token into
.envasGITLAB_TOKEN
config.yaml — copy from config.example.yaml:
github:
username: your-github-username
gitlab:
username: your-gitlab-username # can differ from GitHub
url: https://gitlab.com # change only for self-hosted GitLab
backup:
include_forks: false # include forked repositories? (default: false)
include_archived: true # include archived repositories? (default: true)
temp_dir: ./tmp # temporary dir for clones — auto-cleaned after each repo.env — copy from .env.example:
GITHUB_TOKEN=ghp_...
GITLAB_TOKEN=glpat-...
Tokens are loaded at runtime and never logged. .env is in .gitignore.
# Back up all personal repositories
python backup.py
# Preview what would happen — no writes
python backup.py --dry-run
# Back up only repos matching a pattern
python backup.py --filter "myproject-*"
# Include forks (skipped by default)
python backup.py --include-forks
# Verbose: show raw git output (useful for debugging SSH issues)
python backup.py --verbose
# Use a different config file
python backup.py --config /path/to/config.yamlDRY RUN MODE — no changes will be made
my-private-repo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3/10
Repository Status Details
my-private-repo ~ dry run would create as private
my-public-site ~ dry run would create as public
archived-experiment ~ dry run would create as private
10 dry run
[DRY RUN] Nenhuma operação foi executada.
Private repositories on GitHub are always created as private on GitLab. Visibility is derived exclusively from the GitHub API response — there is no code path that can promote a private repository to public. If visibility cannot be determined, the tool defaults to private and logs a warning.
SSH keys are used for all git operations. API tokens are read from environment variables (.env), never from the config file, and never written to logs.
| Limitation | Details |
|---|---|
| Git LFS | LFS objects are not transferred (git clone --mirror skips them) |
| Organization repos | Not included in v1; planned for v2 with --include-orgs |
| GitLab pull mirroring | Requires GitLab Premium for private repos; a GitHub Actions alternative is planned for v2 |
- v1 — Full backup: clone + push via SSH, idempotent, dry-run, selective filter
- v2 — Auto-sync: GitLab pull mirror (Premium) or GitHub Actions push mirror
- v2 — Organization repos with explicit opt-in
- v3 — Wiki mirroring
Issues and pull requests are welcome. Please open an issue first to discuss what you'd like to change.
MIT — see LICENSE.