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

  1. Create new (+) → New project/repository → Import project → GitHub in GitLab.
  2. Click Authorize with GitHub. Approve the OAuth app.
  3. 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.
  4. Pick repositories and a target GitLab namespace. Optionally rename.
  5. Review the optional toggles.
  6. Click Import. Imports run as background jobs; you can leave the page.

Method 2 — Personal Access Token

  1. Generate a GitHub classic token at github.com/settings/tokens/new with repo (and read:org for collaborators or LFS).
  2. In GitLab: Create new → New project/repository → Import project → GitHub → Authorize with GitHub.
  3. 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

  1. Open the new GitLab project. The status banner shows scheduled / started / finished / partially completed / failed.
  2. If it's partially completed, click through to see which entities failed. Common causes: rate limits, missing permissions, deleted GitHub users.
  3. Compare counts: branches, tags, latest commit SHA, issues, merge requests, labels, milestones.
  4. Imported items show an Imported badge (GitLab 17.2+).
  5. 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:

  1. The importer creates placeholder users for any GitHub author, assignee, or reviewer that doesn't already match a GitLab user.
  2. 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.
  3. The reassigned user receives a notification and must accept before the contributions move to their account. They can also reject.
  4. 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 #NNN for 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.