Migration Guide
End-to-end flow for moving a GitHub repository into GitLab.
End-to-end flow for moving a GitHub repository into GitLab. Pick a method, import, verify, then move on to CI/CD conversion.
Prerequisites
- Maintainer or Owner on the destination GitLab group.
- The "GitHub" import source enabled on the GitLab instance (default on GitLab.com).
- For organization repos: third-party application access not restricted, or you can grant it.
- For collaborator import: Write or Maintain on the GitHub project.
- For LFS: LFS enabled on the destination GitLab project before import.
Choose a method
| Method | Use when | Tokens needed |
|---|---|---|
| GitHub OAuth | GitLab.com or any instance with GitHub OAuth configured. Fastest path. | None — browser flow |
| Personal Access Token | OAuth not available. Required for most GitHub Enterprise Server setups. | GitHub classic PAT with repo (add read:org for collaborators or LFS) |
| REST API | Bulk imports, scripting, public repos you don't own, GitHub Enterprise Server. | The above plus a GitLab PAT with api |
Fine-grained GitHub tokens are not supported.
Method 1 — GitHub OAuth
- Create new (+) → New project/repository → Import project → GitHub in GitLab.
- Click Authorize with GitHub. Approve the OAuth app.
- If you're importing from an organization, click Grant next to the org name on the GitHub authorization page. Without this, the org's repos won't appear.
- Pick repositories and a target GitLab namespace. Optionally rename.
- Review the optional toggles.
- Click Import. Imports run as background jobs; you can leave the page.
Method 2 — Personal Access Token
- Generate a GitHub classic token at
github.com/settings/tokens/newwithrepo(andread:orgfor collaborators or LFS). - In GitLab: Create new → New project/repository → Import project → GitHub → Authorize with GitHub.
- Paste the token, click Authenticate, then continue as with OAuth.
Method 3 — REST API
Single import:
curl --request POST \
--url "https://gitlab.com/api/v4/import/github" \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"personal_access_token": "<github_classic_pat>",
"repo_id": 12345678,
"target_namespace": "my-group",
"new_name": "imported-project",
"optional_stages": {
"single_endpoint_issue_events_import": true,
"single_endpoint_notes_import": true,
"attachments_import": true,
"collaborators_import": true
},
"timeout_strategy": "pessimistic"
}'
optional_stages mirrors the UI toggles. timeout_strategy: pessimistic
aborts partial GitHub API calls instead of waiting them out — useful for very
large repositories.
Track import status:
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/<project_id>/import"
# import_status: scheduled | started | finished | failed
Get the numeric repo_id from GET /repos/{owner}/{repo} on GitHub's API.
Optional toggles
The import dialog exposes three toggles. Each maps to a field in
optional_stages on the API.
| Toggle | API field | Default | Use it when |
|---|---|---|---|
| Use alternative comments import method | single_endpoint_notes_import |
Off | Project has more than ~30,000 comments. Slower but works around GitHub's per-issue API limits. |
| Import Markdown attachments | attachments_import |
Off | You want images and files embedded in descriptions, comments, and releases pulled in as GitLab uploads. |
| Import collaborators | collaborators_import |
On | Default. Imports project members with role mapping. Honors the destination group's "Users cannot be added to projects" setting (GitLab 18.4+). |
Monitor and verify
- Open the new GitLab project. The status banner shows scheduled / started / finished / partially completed / failed.
- If it's partially completed, click through to see which entities failed. Common causes: rate limits, missing permissions, deleted GitHub users.
- Compare counts: branches, tags, latest commit SHA, issues, merge requests, labels, milestones.
- Imported items show an Imported badge (GitLab 17.2+).
- Re-importing creates a fresh copy under a new name. Importing into an existing project isn't supported.
See What Is Migrated for the per-artifact verification list.
User contribution mapping
From GitLab 17.8 onward, the importer no longer requires every contributor to expose their GitHub email. The flow:
- The importer creates placeholder users for any GitHub author, assignee, or reviewer that doesn't already match a GitLab user.
- After import, an admin (or Maintainer/Owner of the destination group) opens the group's Members → Placeholders page and reassigns each placeholder to the real GitLab user.
- The reassigned user receives a notification and must accept before the contributions move to their account. They can also reject.
- Anything not reassigned stays as a placeholder. Reassignment can be re-run later.
Imported @username mentions are wrapped in backticks so they don't fire
notifications. @octocat becomes the literal text `@octocat`
(GitLab 17.5+).
Gotchas
Watch out for these
- Git LFS silently skips if LFS isn't enabled on the destination project. Enable it before import.
- OAuth import hangs when the source organization restricts third-party app access. Grant the app or fall back to the PAT method.
- Pre-2023-05-09 attachments in private GitHub repositories can't be imported (GitHub-side limit).
- GitHub Enterprise Server attachments (GitLab 18.4+): only video and image attachments come across. Other file types are skipped silently.
- SAML SSO accounts can fail to import attachments. Workaround: temporarily add the importing user as an outside collaborator on the GitHub repo.
- Reference syntax: GitHub uses
#NNNfor both issues and PRs; GitLab uses#for issues and!for merge requests. Imported references in descriptions don't autolink, and references in comments default to issues with the same number. - Auto-merged commits may show unverified in GitLab — they're signed with GitHub's internal GPG key.
- GitHub Enterprise Cloud custom repository roles aren't supported. Affected collaborators need to be added manually after import.