A battle‑tested Python toolkit that automates:
- Full‑project exports from a source GitLab instance
- Clean, namespace‑safe imports into a destination GitLab instance
- Post‑import cleanup (fixing wrong namespaces, trimming obsolete branches, enforcing branch standards)
Built with the official python‑gitlab SDK, these scripts have helped teams migrate 50 + micro‑repos in minutes while keeping branch hygiene spotless.
- High-Level Architecture
- Prerequisites
- Quick Start
- Script Walk-Through
- End-to-End Migration Guide
- Troubleshooting
- Extending the Toolkit
- License
flowchart LR
subgraph "Source GitLab"
A((Group: group)) -->|GitLab Export API| B[export.py]
end
B -->|*.tar.gz| Store[(Shared Storage<br/>or Local Disk)]
subgraph "Destination GitLab"
C[import.py / selected_import.py] -->|GitLab Import API| D((Group: group1))
D -->|Namespace Fix| cleanup[cleanup.py]
D -->|Branch Hygiene| branchOps[remove_obsolete_branches.py<br/>specific_project_remove_branches.py]
end
classDef gitlab fill:#FCA326,color:#222,stroke:#E24329
class A,C,D gitlab
📂 Directory layout
.
├── scripts/
│ ├── export.py
│ ├── import.py
│ ├── selected_import.py
│ ├── cleanup.py
│ ├── remove_obsolete_branches.py
│ └── specific_project_remove_branches.py
└── README.md (this file)
- export.py triggers asynchronous exports and downloads resulting archives
- import.py / selected_import.py stream archives into the correct destination group
- cleanup.py deletes projects accidentally imported under default user namespaces
- Branch scripts enforce a single‑source‑of‑truth
demobranch and spawndemo2
| Requirement | Notes |
|---|---|
| Python ≥ 3.8 | Tested on 3.8 → 3.12 |
pip install python‑gitlab |
pip install python-gitlab |
| Personal Access Tokens | api scope on both GitLab instances |
| Export/Import permissions | Admin or Maintainer on all projects |
| Shell access | For running scripts & storing archives |
Store tokens with a secrets manager or environment variables. The scripts will automatically fall back to environment variables if present (see comments inside each script).
# 1 · Clone this repo
$ git clone https://github.com/SubhanshuMG/gitlab-import-export.git
$ cd gitlab-import-export
# 2 · Create & activate virtual‑env
$ python3 -m venv .venv && source .venv/bin/activate
$ pip install -r requirements.txt # currently only python-gitlab
# 3 · Export environment variables (safer than hard‑coding)
$ export SRC_GITLAB=https://gitlab.example.com
$ export SRC_TOKEN=glpat‑xxxxxxxxxxxxxxxx
$ export DST_GITLAB=https://gitlab-dev.example.com
$ export DST_TOKEN=glpat‑yyyyyyyyyyyyyyyy
# 4 · Export all projects under source group
$ python scripts/export.py
# 5 · Import everything into destination group
$ python scripts/import.py # or python scripts/selected_import.py
# 6 · Post‑import fixes
$ python scripts/cleanup.py
$ python scripts/remove_obsolete_branches.py| Step | Purpose |
|---|---|
| Authenticate | Login to $SRC_GITLAB with PAT |
| Discover projects | Lists every project in group1 incl. sub‑groups |
| Trigger export | POST /projects/:id/export |
| Poll status | Waits until export_status == 'finished' |
| Download | Streams tarball in 1 MiB chunks → ./exports/{project}.tar.gz |
| Step | Purpose |
|---|---|
| Auth to destination | $DST_GITLAB PAT |
| Verify target group | Auto‑create if missing (code ready for enhancement) |
| Stream import | POST /projects/import supplying namespace & namespace_id to dodge “root/Administrator” traps |
| Monitor | Polls import_status every 5 s |
| Validate namespace | Warns if import landed outside intended group |
Same as import.py but only for names in SELECTED_PROJECTS—ideal for re‑tries or phased rollouts.
Deletes projects that mistakenly got imported under root or any namespace in WRONG_NAMESPACE_PATHS.
For every repo in a group:
- Verify
demobranch exists - Delete all other branches (auto‑unprotect if needed)
- Create
demo2fromdemo - Set
demoas default branch
Same algorithm as above, but scoped to a single repo defined by TARGET_PROJECT_NAME.
| # | Action | Command / Notes | |
|---|---|---|---|
| 1 | Snapshot / Backup (optional) | Backup runners, project variables, pipeline schedules | |
| 2 | Export | python scripts/export.py → exports/*.tar.gz |
|
| 3 | Inspect archives | `tar -tzf exports/.tar.gz | head` |
| 4 | Import | python scripts/import.py (bulk) or python scripts/selected_import.py (subset) |
|
| 5 | Namespace sanity | python scripts/cleanup.py |
|
| 6 | Branch hygiene | python scripts/remove_obsolete_branches.py or targeted script |
|
| 7 | CI/CD validation | Trigger pipelines, verify secrets, runners | |
| 8 | Audit | Compare commit counts, releases, tags |
| Symptom | Cause & Fix |
|---|---|
GitlabGetError: 404 on group |
PAT lacks read_api scope or wrong TARGET_GROUP_PATH |
| Import stuck at scheduled | Destination Sidekiq queue overloaded or instance runner paused |
/tmp/… no space left |
Move exports to larger disk or set TMPDIR env var |
| Protected branch deletion fails | Script unprotects automatically—ensure you’re Maintainer |
- Overwrite imports: set
overwrite=Trueinimport_params(already done). - Parallel exports: wrap export calls with
concurrent.futures.ThreadPoolExecutor. - CI Variables & Releases: iterate
/projects/:id/variablesand/releasesafter import. - SaaS → Self-managed: add mapping for group paths that differ between instances.
MIT © 2025