| | |

GitLab 24 🦊 Migrating Repositories and Issues from GitHub or Bitbucket to GitLab

⏱️ Estimated read: 12 min | |

A migration is not a clone. A clone copies the Git history. A migration copies the history, the issues, the merge requests, the comments, the labels, the milestones, and the wiki pages, and it attempts to map the original authors to GitLab users. The LFCS exam does not test GitLab, but the GitLab Certified Associate exam does. The critical distinction is between the Git repository import (which only copies code) and the platform-specific importers (which copy the entire project metadata). The GitHub importer and the Bitbucket Cloud importer are the two mechanisms. The Bitbucket Server importer is separate and uses a different workflow. The Repository by URL import is the fallback when the platform-specific importer is unavailable or when only the Git history is needed.

Key point: The GitHub importer requires a GitHub personal access token. The Bitbucket Cloud importer requires the Bitbucket Cloud integration to be enabled on the GitLab instance. Both importers create a new GitLab project; they do not import into an existing project. The user contribution mapping requires the original author to have a public email that matches their GitLab email, or to have connected their account via the OAuth sign-in. If the mapping fails, the project creator is assigned as the author, and a note preserves the original author’s name.


Why migration requires a specific mental model

The metadata problem. A Git repository contains commits, branches, and tags. It does not contain issues, merge requests, comments, labels, or milestones. Those are platform features that are stored in the platform’s database, not in the Git repository. A migration tool must read the source platform’s API, fetch the metadata, and write it to the destination platform’s database. The Git history is the smallest part of the migration.

The user mapping problem. An issue on GitHub has an author. The author is identified by a GitHub username. GitLab needs to know which GitLab user corresponds to that username. The mapping succeeds if the user has a public email on GitHub that matches their GitLab email, or if the user has connected their GitHub account to their GitLab account via the OAuth sign-in. If the mapping fails, the issue is created with the project creator as the author, and a note mentions the original author. The mapping is best-effort, not guaranteed.

The token problem. The GitHub importer requires a GitHub personal access token with the repo scope. The token is used to read the repository and its metadata via the GitHub API. The token must be a classic token, not a fine-grained token. The Bitbucket Cloud importer uses the OAuth integration, which is enabled by default on GitLab.com but may need to be enabled by an administrator on a self-managed instance.

The namespace problem. The import creates a new project in a target namespace. The namespace must exist before the import. The user must have at least the Maintainer role on the target group. The import does not create the group; it only creates the project within it.

The trade-off. The platform-specific importer is the correct mechanism for a full migration. The Repository by URL import is the fallback when the platform-specific importer is unavailable. The Repository by URL import only copies the Git history; it does not copy issues, merge requests, or any other metadata. The Git mirror is the fallback when the import times out on a large repository.


a. The GitHub importer: UI, API, and token

The GitHub importer is available from the GitLab UI under Create new → Import project → GitHub. The UI flow uses OAuth to authenticate with GitHub and then lists the repositories that the user can access.

Create new → Import project → GitHub
Authenticate with GitHub → Grant access
Select repositories → Import

The UI flow is the recommended method for GitLab.com. The OAuth authentication is the simplest path for the user. The importer runs as a background job, and the status can be checked in the import history.

The API flow is the mechanism for bulk imports and for self-managed instances that do not have the OAuth integration configured. The POST /import/github endpoint accepts the GitHub personal access token, the repository ID, the target namespace, and optional stages.

curl --request POST \
  --url "https://gitlab.example.com/api/v4/import/github" \
  --header "content-type: application/json" \
  --header "Authorization: Bearer <gitlab_token>" \
  --data '{
    "personal_access_token": "<github_token>",
    "repo_id": "12345",
    "target_namespace": "group/subgroup",
    "new_name": "new-project-name"
  }'

The personal_access_token is the GitHub token. The repo_id is the numeric ID of the GitHub repository, not the name. The target_namespace is the GitLab group or user namespace. The new_name is the name of the new GitLab project. The optional_stages object can enable additional imports: single_endpoint_notes_import for thorough comment import, attachments_import for Markdown attachments, and collaborators_import for direct collaborators .

The import status is checked with the GET /projects/:id/import endpoint. The status moves through scheduled, started, and finished. A partially completed status means some entities failed to import, and the error details identify which ones .


b. The Bitbucket Cloud importer: OAuth and app password

The Bitbucket Cloud importer is available from the GitLab UI under Create new → Import project → Bitbucket Cloud. The UI flow uses the Bitbucket OAuth integration.

Create new → Import project → Bitbucket Cloud
Sign in to Bitbucket → Grant access
Select repositories → Import

The Bitbucket Cloud importer requires the Bitbucket Cloud integration to be enabled on the GitLab instance. On GitLab.com, it is enabled by default. On a self-managed instance, the administrator must enable it .

The importer can import the repository description, Git repository data, issues with comments, pull requests with comments, milestones, wiki pages, tags, and LFS objects. It cannot import pull request approvals or approval rules. The imported issues and merge requests have an Imported badge in GitLab .

The user mapping for Bitbucket requires two conditions: the username in the Bitbucket account settings must match the public name in the Atlassian account settings, and the user must connect their Bitbucket account in the GitLab profile service sign-in. If the mapping fails, the project creator is set as the author, and a note preserves the original Bitbucket author .

The Bitbucket Cloud importer works only with Bitbucket.org. For Bitbucket Server (Stash), the separate Bitbucket Server importer must be used. The Bitbucket Cloud importer does not support importing from a Bitbucket Server instance .


c. The Repository by URL fallback and Git mirroring

The Repository by URL import is the fallback when the platform-specific importer is unavailable. It copies the Git history but not the issues or merge requests.

Create new → Import project → Repository by URL
Enter the Git repository URL
Enter credentials for private repositories
Create project

The import_url in the API is the Git URL. For a private repository, the URL includes the username and password or token: https://username:token@example.com/group/project.git. The API endpoint is POST /projects with the import_url field .

The git clone --mirror and git push --mirror sequence is the fallback when the import times out on a large repository. The --mirror flag copies all branches, tags, and refs. The sequence is: clone the source with --mirror, add the GitLab remote, and push with --mirror .

Repository mirroring is a different feature. A push mirror copies changes from GitLab to an external repository. A pull mirror copies changes from an external repository to GitLab. Pull mirroring is available in the Premium and Ultimate tiers. The mirror is a synchronization mechanism, not a one-time migration .


Complete Example Session

# ============================================
# PART 1: GITHUB IMPORT VIA UI
# ============================================

# Create new → Import project → GitHub
# Authenticate with GitHub → Grant access
# Select repository → Import
# The import runs as a background job.
# ============================================
# PART 2: CHECK IMPORT STATUS
# ============================================

# The project page shows the import status banner.
# Status: scheduled → started → finished.
# ============================================
# PART 3: GITHUB IMPORT VIA API
# ============================================

curl --request POST \
  --url "https://gitlab.example.com/api/v4/import/github" \
  --header "Authorization: Bearer <gitlab_token>" \
  --header "content-type: application/json" \
  --data '{
    "personal_access_token": "<github_token>",
    "repo_id": "12345",
    "target_namespace": "my-group",
    "new_name": "imported-repo"
  }'
# ============================================
# PART 4: CHECK IMPORT STATUS VIA API
# ============================================

curl --header "PRIVATE-TOKEN: <gitlab_token>" \
  "https://gitlab.example.com/api/v4/projects/<project_id>/import"
# ============================================
# PART 5: BITBUCKET CLOUD IMPORT
# ============================================

# Create new → Import project → Bitbucket Cloud
# Sign in to Bitbucket → Grant access
# Select repositories → Import
# ============================================
# PART 6: VERIFY USER MAPPING
# ============================================

# Check an imported issue:
# If the author is mapped, the original author is shown.
# If not, the project creator is shown with a note.
# ============================================
# PART 7: REPOSITORY BY URL FALLBACK
# ============================================

# Create new → Import project → Repository by URL
# Git repository URL: https://github.com/user/repo.git
# Create project
# ============================================
# PART 8: GIT MIRROR FALLBACK
# ============================================

git clone --mirror https://github.com/user/repo.git
cd repo.git
git remote add new-origin https://gitlab.com/group/repo.git
git push --mirror new-origin
# ============================================
# PART 9: CHECK IMPORT HISTORY
# ============================================

# Create new → Import project → History
# Shows all imports, their status, and any errors.

The nine parts trace the migration workflow: GitHub UI import, status check, GitHub API import, API status check, Bitbucket Cloud import, user mapping verification, Repository by URL fallback, Git mirror fallback, and import history.


Quick Reference

Importers

ImporterSourceMetadataToken
GitHubGitHub.com, GitHub EnterpriseIssues, MRs, wiki, milestones, labelsGitHub PAT (repo scope)
Bitbucket CloudBitbucket.orgIssues, PRs, wiki, milestones, LFSOAuth integration
Bitbucket ServerBitbucket ServerSeparate importer—
Repository by URLAny Git URLGit history onlyOptional for private

Import API

FieldPurpose
personal_access_tokenGitHub token
repo_idGitHub repository ID
target_namespaceGitLab group
new_nameNew project name
optional_stagesAdditional imports

User Mapping

ConditionResult
Public GitHub email matches GitLab emailAuthor mapped
GitHub OAuth connected to GitLabAuthor mapped
No matchProject creator assigned, note added

Fallback

MethodUse Case
Repository by URLNo metadata needed
git clone --mirror + git push --mirrorImport timeout

Best Practices

✅ Do This:

# Use the platform-specific importer for full migration
<!-- Issues, MRs, and metadata are copied -->        # ✅
# Ensure users have public emails before import
<!-- User mapping succeeds -->                       # ✅
# Use the API for bulk imports
<!-- Scriptable and repeatable -->                   # ✅

❌ Don’t Do This:

# Use Repository by URL for a full migration
<!-- Issues and MRs are not copied -->               # ❌
# Use a fine-grained GitHub token
<!-- The importer requires a classic token -->       # ❌
# Expect user mapping without matching emails
<!-- The project creator becomes the author -->      # ❌

Common Pitfalls

PitfallWhy It HappensFix
Import fails with 401Wrong token typeUse a classic GitHub PAT
User mapping failsNo public emailAdd public email or use OAuth
Import timeoutLarge repositoryUse git clone --mirror fallback
Bitbucket Cloud import disabledIntegration not enabledEnable in admin settings
Import into existing projectImporter creates new projectUse a different name or delete the old project

Real-World Examples

1. GitHub UI Import

Create new → Import project → GitHub

2. GitHub API Import

curl --request POST --url "https://gitlab.example.com/api/v4/import/github" --data '{"personal_access_token": "<token>", "repo_id": "12345", "target_namespace": "group"}'

3. Bitbucket Cloud Import

Create new → Import project → Bitbucket Cloud

4. Repository by URL

Create new → Import project → Repository by URL

5. Git Mirror

git clone --mirror https://github.com/user/repo.git

6. Push Mirror

git push --mirror new-origin

7. Check Import Status

curl --header "PRIVATE-TOKEN: <token>" "https://gitlab.example.com/api/v4/projects/<id>/import"

8. Import History

Create new → Import project → History

9. Public Email

GitHub Settings → Emails → Keep my email addresses private (unchecked)

10. Enable Bitbucket Integration

Admin Area → Settings → General → Bitbucket Cloud

Visual

Import Decision Flow

┌──────────────────────────────────────────────┐
│  Source platform?                            │
└──────────┬───────────────────────┬───────────┘
           │ GITHUB                │ BITBUCKET CLOUD
           ▼                       ▼
┌──────────────────────┐  ┌──────────────────────┐
│  GitHub importer     │  │  Bitbucket Cloud     │
│  ├── UI (OAuth)      │  │  importer            │
│  ├── API (PAT)       │  │  (OAuth)             │
│  └── Full metadata   │  │  Full metadata       │
└──────────────────────┘  └──────────────────────┘
           │
           ▼
┌──────────────────────────────────────────────┐
│  Platform importer unavailable?              │
│  ├── YES → Repository by URL (history only)  │
│  └── NO  → Use platform importer             │
└──────────────────────────────────────────────┘

User Mapping

┌──────────────────────────────────────────────┐
│  GitHub user: alice                          │
│  Public email: alice@example.com             │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  GitLab user with email alice@example.com?   │
│  ├── YES → Map to alice (GitLab)             │
│  └── NO  → Assign to project creator         │
│            Add note: "Originally by alice"   │
└──────────────────────────────────────────────┘

Summary

ItemValue
GitHub importerFull metadata
GitHub tokenClassic PAT with repo scope
Bitbucket Cloud importerFull metadata
Bitbucket Cloud requirementIntegration enabled
Repository by URLGit history only
Import createsNew project (not into existing)
User mappingPublic email or OAuth
Import timeout fallbackgit clone --mirror
Import status APIGET /projects/:id/import

Key takeaways:

  • The platform-specific importer is the full migration tool. The GitHub and Bitbucket Cloud importers copy the repository, the issues, the merge requests, the comments, and the metadata. The Repository by URL import copies only the Git history .
  • The GitHub importer requires a classic personal access token. Fine-grained tokens are not supported. The token needs the repo scope. The token is used to read the repository and its metadata via the GitHub API .
  • User mapping is best-effort. The importer maps the original author to a GitLab user if the user has a public email that matches, or if the user has connected their account via OAuth. If the mapping fails, the project creator is assigned as the author .
  • The Bitbucket Cloud importer requires the integration. On GitLab.com, it is enabled by default. On a self-managed instance, the administrator must enable it. The importer works only with Bitbucket.org, not Bitbucket Server .
  • The Git mirror is the fallback for timeouts. A large repository may time out during the import. The git clone --mirror and git push --mirror sequence copies all branches and tags without the platform API .

Remember: A migration is not a clone. The clone copies the code. The migration copies the project. The platform-specific importer is the correct tool for a full migration. The Repository by URL is the fallback for the Git history only. The user mapping is the bridge between the old platform’s users and the new platform’s users. The exam tests the distinction between the importers and the fallbacks.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!