A CLI tool and Python library to migrate Git repositories between providers — preserving full commit history, branches, tags, and Git LFS objects.
Current support: any Git host as source · GitHub as destination
- Full history migration via
git clone --mirror+git push - Batch migration from a JSON file or directly from the GitLab API
- Incremental sync — if the destination is behind the source, missing commits are pushed automatically
- Git LFS support — fetches and pushes LFS objects; auto-converts plain blobs > 100 MB to LFS
- Large repo support — push in configurable commit slices to stay under GitHub's 2 GB per-push limit
- Parallel workers — migrate multiple repositories concurrently
- Organization support — migrate to a personal account or a GitHub organization
- Real-time error reporting — failures written to a CSV file as they occur
| Provider | Clone & push history | Token auth | LFS fetch | Batch via API | Branch listing via API | Archive after sync |
|---|---|---|---|---|---|---|
| GitLab | ✅ Full | ✅ --gitlab-token |
✅ | ✅ --from-gitlab |
✅ | ✅ --archive-synced |
| GitHub | ✅ Full | ✅ --github-token |
✅ | ❌ | ❌ | ❌ |
| Bitbucket | ✅ Full | ✅ | ❌ | ❌ | ❌ |
¹ Bitbucket does not support token-as-password in the same way; credentials must be embedded directly in the HTTPS URL (https://user:app_password@bitbucket.org/...).
| Provider | Repo creation | Push history | LFS push | Org support | Sync check |
|---|---|---|---|---|---|
| GitHub | ✅ | ✅ | ✅ | ✅ | ✅ |
| GitLab | ❌ | ❌ | ❌ | ❌ | ❌ |
| Bitbucket | ❌ | ❌ | ❌ | ❌ | ❌ |
| Symbol | Meaning |
|---|---|
| ✅ | Fully supported |
| Partially supported — works but requires manual steps or workarounds | |
| ❌ | Not supported |
- Python 3.8+
giton PATHgit-lfson PATH (optional — required only for LFS object transfer)
pip install git-provider-migratorOr install from source:
git clone https://github.com/emanuelflp/git-provider-migrator.git
cd git-provider-migrator
pip install -e .All migrations push to GitHub. Create a Personal Access Token with the following permissions:
| Token type | Required permissions |
|---|---|
| Classic | repo (full control) |
| Fine-grained | Contents (read/write) · Metadata (read) · Administration (read/write) |
export GITHUB_TOKEN="ghp_..."Tokens are resolved in this priority order:
| Priority | Source | Details |
|---|---|---|
| 1 | CLI flags | --github-token, --gitlab-token |
| 2 | tokens.csv |
Looked up in the current working directory by default; override with --tokens-csv <path> |
| 3 | Environment variables | GITHUB_TOKEN, GITLAB_TOKEN |
If a token is not found via any of the three sources, the tool exits with an error.
Create a Personal Access Token with the read_repository scope.
export GITLAB_TOKEN="glpat-..."For self-hosted instances, pass the base URL via --gitlab-base-url.
git-provider-migrator \
--source-provider gitlab \
--source-url https://gitlab.com/your-org/your-repo.git \
--repo-name your-repo \
--privategit-provider-migrator \
--source-provider gitlab \
--github-org your-github-org \
--source-url https://gitlab.com/your-org/your-repo.git \
--repo-name your-repoAutomatically fetches the repository list via the GitLab API — no need to maintain a JSON file.
# All repos from a GitLab group
git-provider-migrator --source-provider gitlab --from-gitlab --gitlab-namespace your-group
# All repos from a GitLab user
git-provider-migrator --source-provider gitlab --from-gitlab --gitlab-namespace username
# All repos owned by the authenticated GitLab user
git-provider-migrator --source-provider gitlab --from-gitlab
# From a self-hosted GitLab instance
git-provider-migrator --source-provider gitlab \
--from-gitlab \
--gitlab-base-url https://gitlab.mycompany.com \
--gitlab-namespace your-groupPass --archive-synced to archive each GitLab project once all its branches are in sync:
git-provider-migrator --source-provider gitlab --from-gitlab --archive-syncedCreate a Personal Access Token with the repo scope (classic) or Contents: read (fine-grained).
git-provider-migrator \
--source-provider github \
--gitlab-token ghp_sourcetoken... \
--source-url https://github.com/source-org/your-repo.git \
--repo-name your-repo \
--privateCreate an App Password with Repositories: Read permission and embed it in the clone URL.
git-provider-migrator \
--source-provider bitbucket \
--source-url "https://user:app_password@bitbucket.org/workspace/your-repo.git" \
--repo-name your-repo \
--privateCreate a Personal Access Token with Code: Read scope and embed it in the clone URL.
git-provider-migrator \
--source-url "https://user:token@dev.azure.com/org/project/_git/your-repo" \
--repo-name your-repo \
--privateCreate an API token in Settings → Applications → Access Tokens with repository: read permission.
git-provider-migrator \
--gitlab-token your-gitea-token \
--source-url https://gitea.mycompany.com/user/your-repo.git \
--repo-name your-repo \
--privateAny HTTPS URL with embedded credentials works out of the box:
git-provider-migrator \
--source-url "https://user:token@git.mycompany.com/repo.git" \
--repo-name your-repo \
--privateWorks with any mix of source providers in the same file:
[
{
"source_url": "https://gitlab.com/your-org/frontend.git",
"repo_name": "frontend",
"private": true,
"description": "Frontend application"
},
{
"source_url": "https://github.com/source-org/backend.git",
"repo_name": "backend",
"private": true
},
{
"source_url": "https://user:pass@bitbucket.org/workspace/mobile.git",
"repo_name": "mobile",
"private": true
}
]git-provider-migrator --batch-file repositories.json| Flag | Default | Description |
|---|---|---|
--source-provider |
gitlab |
Source Git provider: gitlab, github, bitbucket |
--dest-provider |
github |
Destination Git provider (only github supported) |
| Flag | Default | Description |
|---|---|---|
--github-token |
$GITHUB_TOKEN |
GitHub personal access token |
--gitlab-token |
$GITLAB_TOKEN |
Source provider personal access token |
--tokens-csv |
./tokens.csv |
Path to a CSV file with github_token and gitlab_token columns (defaults to tokens.csv in the current directory) |
| Flag | Default | Description |
|---|---|---|
--source-url |
— | Source repository HTTPS URL |
--repo-name |
— | Target repository name on GitHub |
--private |
true |
Create the repository as private |
--public |
— | Create the repository as public |
--description |
— | Description for the new repository |
| Flag | Default | Description |
|---|---|---|
--batch-file |
— | Path to a JSON file listing repositories to migrate |
--from-gitlab |
— | Fetch repository list from the GitLab API (requires --source-provider gitlab) |
--gitlab-namespace |
(authenticated user) | GitLab group path or username (only with --source-provider gitlab) |
--gitlab-base-url |
https://gitlab.com |
GitLab instance URL (only with --source-provider gitlab) |
| Flag | Default | Description |
|---|---|---|
--github-org |
(user account) | Migrate into a GitHub organization instead of the user account |
| Flag | Default | Description |
|---|---|---|
--archive-synced |
false |
Archive the source project after all branches sync (GitLab only) |
--workers |
1 |
Number of parallel migration threads |
--commits-per-slice |
(disabled) | Push branches in slices of N commits. Use only when the push fails due to GitHub's 2 GB limit (e.g. --commits-per-slice 200). A warning is shown automatically when the mirror exceeds 2 GB. |
--skip-lfs |
false |
Skip Git LFS binary object transfer |
--errors-output |
migration_errors_<ts>.csv |
Path for the error report CSV |
--debug |
false |
Enable verbose DEBUG logging |
from migrator.clients.github import GitHubMigrator
migrator = GitHubMigrator(github_token="ghp_...", github_org="my-org")
# Single repository (any source)
success, reason = migrator.migrate_repository(
source_url="https://gitlab.com/your-org/your-repo.git", # any git URL
repo_name="your-repo",
gitlab_token="glpat-...", # source token, if needed
private=True,
)
# Multiple repositories (mixed sources)
results = migrator.migrate_repositories(
repositories=[
{"source_url": "https://gitlab.com/org/repo1.git", "repo_name": "repo1", "private": True},
{"source_url": "https://github.com/source-org/repo2.git", "repo_name": "repo2", "private": True},
{"source_url": "https://user:pass@bitbucket.org/ws/repo3.git","repo_name": "repo3", "private": True},
],
gitlab_token="glpat-...",
workers=4,
)
for repo, info in results.items():
print(repo, "✓" if info["success"] else f"✗ {info['reason']}")- Compare — if the repository already exists on GitHub, all branches are compared against the source. If GitHub is up to date, the migration is skipped.
- Clone — the source is cloned with
git clone --mirrorinto a temporary directory. - LFS — existing LFS objects are fetched from the source; plain blobs larger than 100 MB are automatically converted to LFS via
git lfs migrate import. - Push — all refs are pushed with
git push --mirrorby default. For repositories that exceed GitHub's 2 GB per-push limit, use--commits-per-slice Nto push branches in commit slices. - LFS push — LFS objects are pushed to GitHub with
git lfs push --all. - Cleanup — the temporary clone is removed.
Contributions are welcome. Please open an issue to discuss significant changes before submitting a pull request.
git clone https://github.com/emanuelflp/git-provider-migrator.git
cd git-provider-migrator
pip install -e .MIT — see LICENSE.