Migrating Repositories Between Providers
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
This page shows how to move a repository from one Git host to another — with every commit, branch and tag, plus
its Git LFS objects — using nothing but Git itself. The worked example moves the fictitious Bitbucket Cloud
repository acme-legacy/inventory-api to the fictitious GitHub organization acme-labs, but the same flow works
between any two Git hosts. It targets Git 2.55.x, Git LFS 3.x, GitHub CLI (gh) 2.101.x, IntelliJ IDEA 2026.2,
current stable VS Code, and github.com and bitbucket.org as of October 2026. Changing who owns a repository
within GitHub is a different, simpler operation, covered in
Transferring Repositories Between
GitHub Accounts.
Concept
Normal, bare and mirror clones
A Git repository is its history plus its refs (see Repositories and Cloning). Moving it to another host means copying all the refs, not just the branch you happen to have checked out — and the three kinds of clone differ exactly in which refs they copy:
| Clone | What it copies | Good for |
|---|---|---|
|
Every commit reachable from the remote’s branches and tags, but the remote’s branches land as remote-tracking
branches ( |
Day-to-day work. |
|
No working tree; the remote’s branches are copied directly to local branches, and tags are copied. Other refs (notes, remote-tracking branches on the source, host-specific refs) are not. |
A one-off copy of branches and tags (this is what GitHub’s own Duplicating a repository guide uses). |
|
Implies |
A complete migration, or a mirror kept in sync over time. |
Derived from git-clone (--bare, --mirror).
What a Git migration does not move
git push --mirror moves everything that is Git: commits, branches, tags and other refs. Everything a hosting
provider stores next to the Git data stays behind and must be recreated or migrated separately:
-
pull requests and their review comments, and issues;
-
wikis (on both Bitbucket Cloud and GitHub a wiki is a separate Git repository, which you can mirror the same way if you need it);
-
CI/CD configuration that lives outside the repository (Bitbucket Pipelines variables, deployment environments);
-
branch protection / branch restrictions, merge checks and default reviewers;
-
users, groups, teams and permissions;
-
webhooks, deploy keys, repository variables and secrets;
-
Git LFS objects — these live in a separate store and need their own fetch and push (see Git LFS and large files).
GitHub also offers tools that move some of this metadata. GitHub Enterprise Importer migrates repositories including pull requests and other metadata into GitHub Enterprise Cloud, but from Bitbucket it supports only Bitbucket Server and Data Center, not Bitbucket Cloud. GitHub Importer (Import repository on github.com) imports a Git repository from a public URL, but only the code and its history — no issues or pull requests — and it does not move Git LFS objects; the REST API that used to drive it (Source imports) has been retired. The manual flow on this page works with any host, private networks included, and is the one to understand even when you use a tool. See About GitHub Enterprise Importer and About GitHub Importer.
The local mirror is only a transit copy: once the target is verified, it is deleted and every developer re-points their existing clone at the new host.
Command line
The steps below move acme-legacy/inventory-api from Bitbucket Cloud to acme-labs/inventory-api on GitHub.
Replace the workspace, organization and repository names with yours.
1. Create the empty target repository
gh repo create acme-labs/inventory-api --private --description "Inventory API (migrated from Bitbucket)"
Create it empty: no README, .gitignore or license, otherwise the first git push --mirror would have to
overwrite GitHub’s initial commit. Derived from gh repo create and
Duplicating a
repository.
2. Get a read credential for the source
Bitbucket Cloud has retired app passwords in favour of API tokens (personal, tied to your Atlassian account) and access tokens (repository, project or workspace level, tied to that resource). For a one-off migration, a repository access token with the Repositories: Read permission (plus Pull requests: Read if you will export pull requests separately) is the narrowest choice; create it as described in On Bitbucket, copy it once (Bitbucket never shows it again), and give it a short expiry.
With an access token, Git over HTTPS uses the literal username x-token-auth and the token as the password. Put
the username in the URL, never the token: a token in the URL ends up in your shell history and in the clone’s
config file. See Using access tokens for
a repository and the
repository access token
permissions. If you authenticate to Bitbucket with SSH keys or Git Credential Manager already, you can use that
instead and skip the token.
3. Keep credentials per repository
git config --global credential.https://bitbucket.org.useHttpPath true
By default a credential helper stores one credential per host, so a repository access token cached for
inventory-api would be offered for every other bitbucket.org repository too (and an older cached app password
would be offered for this one). With useHttpPath set for bitbucket.org only, the helper keys credentials by the
full repository path. Derived from gitcredentials
(credential.useHttpPath and credential.<url>.*).
4. Mirror-clone the source
git clone --mirror https://x-token-auth@bitbucket.org/acme-legacy/inventory-api.git
# Password prompt: paste <BITBUCKET_REPOSITORY_ACCESS_TOKEN>
cd inventory-api.git
The result is a bare repository directory, inventory-api.git, holding every ref the source advertises. Derived
from git-clone.
5. Fetch the Git LFS objects (only if the repository uses LFS)
git lfs ls-files --all | head # any output means the history references LFS objects
git lfs fetch --all # download every LFS object referenced by any ref
A mirror clone copies the LFS pointer files but not the objects they point to. Details, and what to do with files that are too large for GitHub, are in Git LFS and large files.
6. Push everything to the target
git push --mirror https://github.com/acme-labs/inventory-api.git
git lfs push --all https://github.com/acme-labs/inventory-api.git # only if step 5 applied
git push --mirror pushes every ref under refs/: new refs are created, changed refs are force-updated and refs
that exist on the target but not locally are deleted. Against an empty target that is exactly what you want;
against a repository that already has content, it destroys whatever the mirror does not contain. This is the same
sequence as GitHub’s Duplicating a repository guide. If the push is rejected with deny updating a hidden ref,
see Common mistakes / tips. Derived from git-push (--mirror) and
Duplicating a
repository.
7. Verify
git ls-remote --heads https://x-token-auth@bitbucket.org/acme-legacy/inventory-api.git | sort > /tmp/src-heads
git ls-remote --heads https://github.com/acme-labs/inventory-api.git | sort > /tmp/dst-heads
diff /tmp/src-heads /tmp/dst-heads && echo "branches match"
git ls-remote --tags https://x-token-auth@bitbucket.org/acme-legacy/inventory-api.git | sort > /tmp/src-tags
git ls-remote --tags https://github.com/acme-labs/inventory-api.git | sort > /tmp/dst-tags
diff /tmp/src-tags /tmp/dst-tags && echo "tags match"
gh repo view acme-labs/inventory-api --json defaultBranchRef --jq .defaultBranchRef.name
Branch and tag names and the commit IDs they point to must match; commit IDs only change if history was rewritten
(for example by git lfs migrate import). Check that the GitHub default branch is the one the team used on
Bitbucket, and, for LFS, clone the new repository into a scratch directory and run git lfs fsck there. Derived
from git-ls-remote.
9. Re-point existing working copies
Each developer keeps their clone, local branches and stashes, and only changes the URL of origin:
git remote set-url origin https://github.com/acme-labs/inventory-api.git # or git@github.com:acme-labs/inventory-api.git
git remote -v
git fetch --prune origin
git branch -u origin/main main # only if the default branch name changed
Derived from git-remote.
10. Finish the move
-
Recreate branch protection (or rulesets), required reviews and status checks on GitHub.
-
Port CI: Bitbucket Pipelines (
bitbucket-pipelines.yml) does not run on GitHub; rewrite it as a GitHub Actions workflow and recreate its variables as Actions secrets and variables. -
Recreate webhooks, deploy keys and integrations, and update anything that hard-codes the old URL (build scripts, submodule URLs in
.gitmodules, package manifests, badges in the README). -
Archive the Bitbucket repository, or remove write access to it, so nobody keeps pushing to the old location.
-
Revoke the repository access token.
Git LFS and large files
Git LFS replaces large files in the Git history with small text pointer files and stores the real content in
a separate LFS store on the host. Which paths are stored that way is recorded in .gitattributes:
git lfs install # once per machine: installs the LFS filters and hooks
git lfs track "*.psd" # appends a rule to .gitattributes
git add .gitattributes
git lfs ls-files # LFS files in the current commit
Because the content is outside Git, git clone --mirror and git push --mirror move only the pointers. A
migration therefore needs the order shown in the command-line steps: git lfs fetch --all (download every LFS
object referenced by any ref) → git push --mirror → git lfs push --all <target-url> (upload them all). Check
the result with git lfs ls-files --all on the mirror and with git lfs fsck in a fresh clone of the target. If
the target host rejects the ref push because the LFS objects it references are not there yet, run
git lfs push --all <target-url> first and the mirror push second. See
git-lfs.com,
the Git LFS man pages and GitHub’s
About
Git Large File Storage.
Files over GitHub’s limit that are not in LFS
Bitbucket and GitHub limit large files differently. GitHub warns about any file larger than 50 MiB in a push and
blocks any file larger than 100 MiB, anywhere in the history being pushed — so an old, long-deleted 120 MiB
archive still in a commit on some branch makes git push --mirror fail. Find such files first, in the mirror:
git lfs migrate info --everything --above=50MB --top=20
Then convert them to LFS, rewriting the whole history:
git lfs migrate import --everything --include="*.zip,*.psd" # by pattern
git lfs migrate import --everything --above=100MB # or by size (cannot be combined with --include)
--everything rewrites every local ref, which in a mirror clone is every ref. Two consequences:
-
Every commit SHA from the first converted file onwards changes. Links to commits, signed commits and tags, and every existing clone stop matching. Collaborators must re-clone the new repository instead of re-pointing their old clone, or they will push the old, unconverted history back.
-
The converted files now count towards GitHub’s Git LFS storage and bandwidth, which are billed per account; the per-file maximum for LFS also depends on the plan. Check the current figures in About large files on GitHub and About billing for Git Large File Storage rather than relying on a number copied here.
Derived from git-lfs-migrate and About large files on GitHub.
On GitHub.com
Preparing the target
-
Click + in the top-right corner and choose New repository.
-
Choose the owner (
acme-labs) and the name (inventory-api) and the visibility, and leave Add a README file, .gitignore template and license empty. -
Click Create repository. Ignore the "push an existing repository" snippet GitHub shows; use the mirror push instead.
After the push
-
On the repository’s main page, open the branch dropdown and the Tags view to spot-check that every branch and tag is there (the command-line comparison in step 7 is the complete check).
-
Under Settings, set the Default branch if it is not the one the team used.
-
Recreate branch protection rules or rulesets under Settings, and the Actions secrets and variables under Settings → Secrets and variables → Actions.
GitHub’s settings pages are rearranged from time to time; if a label differs, follow Configuring branches and merges in your repository and Using secrets in GitHub Actions.
On Bitbucket
Creating a repository access token
-
Open the repository on bitbucket.org (
acme-legacy/inventory-api). -
In the left sidebar, next to the repository name, open the … menu and choose Settings.
-
Under Security, choose Access tokens, then Create access token.
-
Give it a name (for example
github-migration), an expiry date a few days away, and the Repositories: Read permission only (add Pull requests: Read if you will export pull requests separately). -
Click Create and copy the token immediately — it is shown only once.
Derived from Create a repository access token. A personal API token (created from your Atlassian account settings) works too but reaches every repository your account can see, so prefer the repository token. App passwords, which older guides use for this step, are retired; see Bitbucket Cloud transitions to API tokens.
In IntelliJ IDEA
The migration itself is a command-line job; inside the IDE you only re-point an existing project:
-
Choose Git | Manage Remotes.
-
Select
origin, click the edit (pencil) icon, replace the Bitbucket URL withhttps://github.com/acme-labs/inventory-api.git(or the SSH form), and click OK. -
Run Git | Fetch to load the branches from the new host.
Derived from JetBrains — Set up a Git repository (Add a remote repository).
In VS Code
VS Code has no "edit remote" command, so re-point origin by removing it and adding it again:
-
Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) and run Git: Remove Remote, choosing
origin. -
Run Git: Add Remote, paste
https://github.com/acme-labs/inventory-api.git, and name itorigin. -
Run Git: Fetch, then set the upstream of your local branches on the next push (VS Code offers Publish Branch or sets it automatically).
Alternatively run git remote set-url origin <new-url> in the integrated terminal, which keeps the upstream
settings. Derived from VS Code — Source Control.
Common mistakes / tips
-
Putting the token in the clone URL.
https://x-token-auth:<token>@...works, but the token then sits in shell history and in the mirror’sconfig. Use the username-only URL and let Git prompt, or a credential helper. -
Mirror-pushing into a non-empty repository.
git push --mirrordeletes every target ref the local mirror does not have, including a README commit made at creation time or branches someone already pushed. Always push into an empty repository, and double-check the URL. -
Hidden refs rejected (
deny updating a hidden ref). A mirror clone may contain host-specific refs, such as pull-request refs (refs/pull-requests/*from some Bitbucket versions, orrefs/pull/*when the source is a GitHub repository). The target host reserves its own pull-request namespace and rejects the push. Delete those refs from the mirror before pushing, or push only branches and tags instead of--mirror:git for-each-ref --format="%(refname)" refs/pull refs/pull-requests | xargs -r -n 1 git update-ref -d # or, instead of --mirror: git push https://github.com/acme-labs/inventory-api.git "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*" -
Forgetting LFS. Without
git lfs fetch --allandgit lfs push --all, the new repository contains only pointer files, and every checkout of an LFS path fails with a missing-object error. Verify in a fresh clone. -
Rewriting history without telling anyone. After
git lfs migrate import, old clones contain the old commits; onegit push --forcefrom such a clone puts the large files back. Announce the cut-over and have everyone re-clone. -
Leaving the token alive. Revoke the repository access token when the migration is verified, and remove the
useHttpPathsetting if you do not need it any more (git config --global --unset credential.https://bitbucket.org.usehttppath).
For moving a repository between GitHub accounts or organizations, where no Git copy is needed at all, see Transferring Repositories Between GitHub Accounts.
References
-
git-scm.com — git-clone (
--bare,--mirror) -
git-scm.com — git-push (
--mirror) -
git-scm.com — gitcredentials (
useHttpPath) -
Git LFS and Git LFS man pages, including git-lfs-migrate