Migrate to git.eu
Copy GitHub, GitLab, git.eu or any git repositories, with all branches and tags, into your git.eu account with the giteu-migrate command-line tool.
Migrate to git.eu
giteu-migrate copies git repositories, with all branches and tags, from GitHub, GitLab,
another git.eu instance or any git server into your account on a git.eu instance - the
destination. It is a command-line tool: it creates each repository on the destination, then
pushes the full history.
source (GitHub / gh / GitLab / git.eu / URL list)
│ generate download import
▼ ▼ ▼
migration.listing.json ─> staging/<slug>.bundle ─> destination (create + push --mirror)
- Start here:
giteu-migrate startasks what it needs and runs the three stages for you. - Advanced: run the stages yourself, one by one or all at once with
run. - One repository, by hand: no tool, only
git- see Migrating a repository.
In the commands below, <INSTANCE> is your instance slug (for example fr): it resolves by
itself when you are signed in on this site.
What You Need
- Node.js 22 or newer.
git.git-lfs, only when a repository uses Git LFS.
The tool checks them first. A missing tool stops it with its install command:
Linux
Install them with your package manager, for example on Debian / Ubuntu:
sudo apt install nodejs git git-lfs
Your distribution's Node.js may be older than 22: then install it from nodejs.org.
macOS
Windows
With winget, in PowerShell:
winget install OpenJS.NodeJS.LTS
winget install --id Git.Git -e
winget install GitHub.GitLFS
Git for Windows already ships git-lfs. Open a new terminal after an install, so it finds the new commands.
Check with node --version, git --version and git lfs version.
Get the Tool
No install, no build, no dependency to fetch.
git clone https://git.eu/gitdoteu/migratetogiteu.git
cd migratetogiteu/migratetogiteu
node bin/giteu-migrate.js --help
Or take the one file
dist/giteu-migrate.js
from the repository, copy it anywhere and run node giteu-migrate.js --help there. Below,
giteu-migrate stands for node bin/giteu-migrate.js or node giteu-migrate.js.
Start Here
In an empty directory, on a terminal:
giteu-migrate start
Run it again until it says the migration is finished. Each launch does the next missing step only:
| Launch | Step |
|---|---|
| 1 | Checks Node.js and git, then asks its questions and writes migrate.config.json and migrate.secrets.json |
| 2 | Checks the tokens and the destination, then writes migration.listing.json |
| 3 | Checks again, then downloads every repository into staging/ |
| 4 | Checks again, shows the import plan, asks you to confirm, creates and pushes, then reports |
| 5+ | Prints the status: nothing left to do |
The first launch asks:
- Copy to an instance, or local backup only (download, no destination).
- The source: GitHub, GitLab or another git.eu instance, and whose repositories (an organization, a user or a group).
- The destination: your instance address (
https://<INSTANCE>.git.eu) and the push method (HTTPS or SSH). - Each missing token. It shows the token page and the scope to check, reads the token hidden, and checks it as you type it. A refused token is asked again, with its cause and fix.
- Existing logins are offered, and used only when you say yes:
gh auth token,GITHUB_TOKEN/GITLAB_TOKEN, and your SSH agent for the SSH method. - A relaunch asks only what is missing - a token removed from the secrets file, for example.
- The source and the destination can not be the same instance.
- Nothing is written to the destination before you confirm the import plan.
- Without a terminal (CI, a pipe),
startstops and prints the same steps as commands. - A
ghoutput file or a URL list: run itsgeneratecommand once by hand, thenstarttakes over.
The final report lists each repository with its link on the destination, each failure with its cause, and one command that retries the failed ones:
Report:
✔ tool: https://<INSTANCE>.git.eu/<USERNAME>/tool
✘ notes: disk_init_failed: full
Retry the 1 failed: giteu-migrate import migration.listing.json --dest-url https://<INSTANCE>.git.eu --only notes
Good to Know First
- Repositories land in the destination API token's own account. No organization destination yet.
- Name rule: letters, digits,
.,_,-. Only a leading/trailing symbol, two symbols in a row or a.git/.atom/.htmlsuffix gets rewritten, and every rewrite is listed in the dry-run. Collisions stop the run. Use--rename-mapto rename. - Every error ends with a
Fix:line: the action to take, and a link to the matching section of this page.
Tokens and Scopes
git.eu takes personal access tokens. A copy to an instance needs two scopes on the destination:
| Scope | Used for | Environment | Secrets file key |
|---|---|---|---|
| API access | Creating the repositories | GITEU_DEST_API_TOKEN | dest.apiToken |
| Git access | Pushing over HTTPS | GITEU_DEST_GIT_PAT | dest.gitPat |
- Git access is checked by default; the two scopes are not interchangeable. One token with both boxes checked fills both.
- Push over SSH instead:
--dest-method ssh, withGITEU_DEST_SSH_KEY(dest.sshKey) set to a private key path, or your SSH agent. The API token is still needed.
A git.eu source needs the same two scopes on the source instance:
| Scope | Used for | Environment | Secrets file key |
|---|---|---|---|
| API access | Listing your repositories | GITEU_SOURCE_API_TOKEN | sources.giteu.apiToken |
| Git access | Cloning over HTTPS | GITEU_SOURCE_GIT_PAT | sources.giteu.gitPat |
Clone over SSH instead with --source-method ssh and GITEU_SOURCE_SSH_KEY
(sources.giteu.sshKey), or your SSH agent.
Other sources:
| Source | Token | Environment | Secrets file key |
|---|---|---|---|
GitHub, gh | A GitHub token: private repositories, higher rate limits | GITHUB_TOKEN | sources.github.token |
| GitLab | A GitLab token with read_api + read_repository | GITLAB_TOKEN | sources.gitlab.token |
| GitLab over SSH | A private key path, or your SSH agent | GITLAB_SSH_KEY | sources.gitlab.sshKey |
| URL list | Your own git credentials and SSH agent | - | - |
Without a GitHub or GitLab token, only public repositories are listed and cloned.
start asks for each token and stores it in the secrets file. For the
advanced path, set them in the secrets file or in the environment.
Shell:
export GITEU_DEST_API_TOKEN=giteu_… # API access
export GITEU_DEST_GIT_PAT=giteu_… # Git access
PowerShell:
$env:GITEU_DEST_API_TOKEN = "giteu_…" # API access
$env:GITEU_DEST_GIT_PAT = "giteu_…" # Git access
- The git.eu API - API tokens, scopes, responses and errors.
- API Reference - every
/api/v0endpoint the tool calls. - Clone and Push over HTTPS - use a token with
git.
The Destination
The destination is never guessed. doctor (destination checks), import, run and status
stop before any call without --dest-url (or destUrl in the config file). init, generate
and download need none.
giteu-migrate doctor --dest-url https://<INSTANCE>.git.eu
A failed destination check names the URL and a plain cause:
| Cause | Meaning | Fix |
|---|---|---|
No git.eu API at this address (404) | The URL is not a git.eu instance | Give your instance address in --dest-url |
Token refused (401) | The API token is wrong, expired or revoked | Create a new token on the destination |
Token lacks the scope (403) | The API token has no API access | Check API access on the token |
| Unreachable | Network error, or a typo in the host name | Check the address and your connection |
doctor --selftest also creates a private repository giteu-migrate-selftest-<stamp>, pushes
one empty commit to it and deletes it.
The Secrets File
migrate.secrets.json holds the tokens and SSH key paths, next to the config file
(--secrets <file> names another one). Every command reads it; a missing file means no stored
secret. An environment variable overrides its key (see Tokens and Scopes).
{
"schemaVersion": 1,
"dest": { "apiToken": "", "gitPat": "", "sshKey": "" },
"sources": {
"github": { "token": "" },
"giteu": { "apiToken": "", "gitPat": "", "sshKey": "" },
"gitlab": { "token": "", "sshKey": "" }
}
}
- Never commit it, never share it: add it to
.gitignore. - It must be owner-only.
startandinitwrite it so; any command refuses a looser file and prints the command that fixes it.
Linux / macOS (mode 0600):
chmod 600 migrate.secrets.json
Windows (an ACL naming only your account), in cmd:
icacls "migrate.secrets.json" /inheritance:r /grant:r "%USERNAME%:F"
or in PowerShell:
icacls "migrate.secrets.json" /inheritance:r /grant:r "${env:USERNAME}:F"
The Config File
migrate.config.json holds no secret: you can share it. Every key but schemaVersion is
optional; a command-line option overrides its key.
{
"schemaVersion": 3,
"destUrl": "https://<INSTANCE>.git.eu",
"dest": { "method": "https" },
"sourceType": "github",
"staging": "staging",
"logDir": "logs",
"sources": {
"github": { "org": "acme" },
"giteu": { "url": "", "cloneMethod": "https" },
"gitlab": { "url": "https://gitlab.com", "cloneMethod": "https" }
}
}
| Key | Option | Use |
|---|---|---|
destUrl | --dest-url | The destination instance, no default |
dest.method | --dest-method | Push over https (default) or ssh |
sourceType | --source-type | The source of generate / run |
sources.github.org / user | --org / --user | The GitHub owner |
sources.gitlab.user / group | --user / --group | The GitLab owner |
sources.giteu.url / cloneMethod | --source-url / --source-method | The git.eu source and its clone method |
sources.gitlab.url / cloneMethod | --source-url / --source-method | The GitLab server and its clone method |
staging / logDir | --staging / --log-dir | Where the bundles and the logs go |
The config is version 3. An older config is refused: run giteu-migrate init --overwrite
and copy your values. Version 3 renames baseUrl to destUrl and import.pushMethod to
dest.method, and moves credentials to the secrets file.
Pick a Source
--source-type (or sourceType in the config) picks the source:
| Source | Options | Reads |
|---|---|---|
| GitHub | --source-type github --org <org> or --user <user> | An organization or user's repositories |
gh CLI output | --source-type gh-cli --source-file <file> | A gh repo list --json file |
| GitLab | --source-type gitlab --group <group> or --user <user> | A GitLab group's or user's projects |
| git.eu instance | --source-type giteu --source-url <url> | Your own repositories on that instance |
| URL list | --source-type urls --source-file <file> | One git URL per line |
An option of another source is refused (--org needs --source-type github.).
From GitLab
--source-type gitlab reads the projects of a GitLab user or group, on gitlab.com or a
self-hosted GitLab.
--source-url(orsources.gitlab.url, defaulthttps://gitlab.com) names the server.--source-method https|ssh(orsources.gitlab.cloneMethod, defaulthttps) picks how to clone.--user <name>: that user's projects; private ones too when the token belongs to that user, public ones only otherwise.--group <path>: the group's projects, subgroups included (full path:acme/team). Projects shared into the group from elsewhere are left out.- Exactly one of
--user/--group. - GitLab
internalvisibility becomes private on git.eu. - Two projects with the same path in different subgroups are a name collision:
generatereports it and writes no listing. Migrate each subgroup with its own--group acme/team(and a--rename-mapwhen the names still clash). - A
429(rate limit) stops the run: retry later.
export GITLAB_TOKEN=glpat-… # read_api + read_repository
giteu-migrate run --source-type gitlab --group acme --dest-url https://<INSTANCE>.git.eu --dry-run
$env:GITLAB_TOKEN = "glpat-…" # read_api + read_repository
giteu-migrate run --source-type gitlab --group acme --dest-url https://<INSTANCE>.git.eu --dry-run
From a git.eu Instance
--source-type giteu reads the repositories of the source token's own account on a git.eu
instance, private ones included. No other user, no organization.
--source-url(orsources.giteu.url) names the source instance.--source-method https|ssh(orsources.giteu.cloneMethod, defaulthttps) picks how to clone, independent of--dest-method.- HTTPS clones need the source Git access token (
GITEU_SOURCE_GIT_PAT): without it,downloadstops before any clone. --no-forksand--no-archiveddo not apply;--visibilitydoes.importandrunrefuse a source and a destination on the same instance (same host name): that would push onto the source repositories.
Instance to instance: the source and destination tokens, and another instance in
--dest-url.
giteu-migrate run --source-type giteu --source-url https://git.plume.one --dest-url https://<INSTANCE>.git.eu --dry-run
Local backup: generate and download need no destination.
giteu-migrate generate --source-type giteu --source-url https://<INSTANCE>.git.eu
giteu-migrate download
From GitHub, gh or a URL List
giteu-migrate run --source-type github --org acme --dest-url https://<INSTANCE>.git.eu --dry-run
gh repo list acme --json name,url > repos.json
giteu-migrate generate --source-type gh-cli --source-file repos.json
giteu-migrate generate --source-type urls --source-file repos.txt
A URL list (repos.txt) holds one clone URL per line, written in any text editor.
The Listing File
migration.listing.json lists the source and each repository to copy: its name on the
destination, its visibility and its description. generate writes it (--listing <file>
names another one); download, import and status read it. You can edit it before the
download.
- A listing that fails its checks is refused with each problem; correct it, or run
generateagain with--overwrite. - A version 1 listing is refused: generate it again.
giteu-migrate generate --template listing,urlsorrename-mapwrites an example to edit.- Descriptions over 500 characters: that repository is excluded, never truncated.
Visibility
- A repository whose visibility is unknown is created private.
- GitHub,
gh, GitLab and git.eu sources carry the source visibility. - A URL list carries none. Add
publicorprivateafter the URL to set it:
https://github.com/acme/tool.git
https://github.com/acme/docs.git public
- Here
toolis created private anddocspublic. Any other word is an error. --visibility public|private|allonly filters which repositories to migrate; it never changes a visibility.
Advanced: Stage by Stage
start runs these commands for you. Run them yourself for scripts, CI, or a URL list:
| Command | Does | Needs --dest-url |
|---|---|---|
init | Writes an example config, an owner-only secrets file and an example listing | No |
doctor [listing] | Checks the tools, the tokens, the destination and the listing | For the destination checks |
generate | Stage 1: builds the listing from a source | No |
download [listing] | Stage 2: copies every listed repository into staging/ as a git bundle | No |
import [listing] | Stage 3: creates the repositories on the destination and pushes the bundles | Yes |
run | generate, download and import in sequence | Yes |
status [listing] | Shows each listed repository: staged, created, pushed | Yes |
giteu-migrate init
giteu-migrate doctor --dest-url https://<INSTANCE>.git.eu --selftest
giteu-migrate run --source-type urls --source-file repos.txt --dest-url https://<INSTANCE>.git.eu --dry-run
giteu-migrate run --source-type urls --source-file repos.txt --dest-url https://<INSTANCE>.git.eu
giteu-migrate status --dest-url https://<INSTANCE>.git.eu
--dry-runshows the full plan and changes nothing.-yskips the confirmation (scripts, CI).importends with the same report asstart, and its retry command (--only <names>).- Write
destUrlinmigrate.config.jsonto drop--dest-url. - The full tutorial with the output of each command lives in the giteu-migrate README.
Command Options
giteu-migrate --help lists the commands and the global options; giteu-migrate <command>
--help lists the options of one command.
| Option | Commands | Use |
|---|---|---|
--dest-url <url> | doctor, import, run, status | The destination instance |
--dest-method <method> | import, run | https (token, default) or ssh (key) |
--source-type <type> | generate, run | github, giteu, gitlab, gh-cli, urls |
--listing <file> | generate, run | The listing file to write |
--overwrite | init, generate, run | Replace a file that already exists |
--redownload | download, run | Download again a repository already staged |
--only / --skip <name,...> | download, import, run | Process only / leave out these repositories |
--force-push | import, run | Push even when the destination holds other history |
--config / --secrets <file> | all | Another config or secrets file |
An option name that changed is refused as unknown, and its Fix: line names the new one:
| Old name | New name |
|---|---|
--source | --source-type |
--base-url | --dest-url |
--push-method | --dest-method |
--force | --overwrite (init, generate), --redownload (download) |
generate --out / -o | --listing |
--resume, --skip-existing, --forks, --archived | Nothing: the default (--no-forks, --no-archived stay) |
GITEU_API_TOKEN / GITEU_GIT_PAT | GITEU_DEST_API_TOKEN / GITEU_DEST_GIT_PAT |
Staging and Resume
download copies each repository into staging/<slug>.bundle (--staging <dir> or staging
in the config moves it). import pushes from there.
- Resume: run the same stage again. Staged, created and pushed work is skipped;
download --redownloadstages again. - A destination repository holding only part of the refs (an interrupted push) is finished, no
--force-pushneeded. - A destination repository that already holds other history is skipped unless
--force-push. - The first SSH access error stops
import/run. Fix the access (ssh -T git@<host>), then run the same command again: it resumes.
./migrate.config.json
./migrate.secrets.json owner-only, never committed
./migration.listing.json
./staging/ <slug>.bundle per repository
./logs/YYYYMMDD.HHMMSS.log one per run
Repositories with Git LFS
A repository that uses Git LFS migrates with its LFS objects.
- A repository uses LFS when a
.gitattributesfile of any branch or tag holdsfilter=lfs(a commented line does not count). Other repositories never call git-lfs. - git-lfs must be installed on the machine that runs
giteu-migrate(see What You Need). Without it, each LFS repository fails with a clear message and the run goes on with the others.doctorshows the git-lfs version, or anot installedwarning. downloadchecks the staging disk has room for the objects and the bundle first.- An object the source no longer has fails that repository, with each missing object id and path.
importuploads the LFS objects before the branches and tags. Anup to daterepository showslfs check: the upload runs again and sends only what git.eu lacks.- Each LFS object has a size ceiling, set by each instance. A larger object fails that repository with a
too largemessage, before any branch or tag is pushed. - Each LFS listing, download or upload call may run up to 6 h.
Getting Help
- Run the same command again: a network error is often gone the second time.
- Each run writes
logs/YYYYMMDD.HHMMSS.log. It records every step; tokens are redacted in it. Send it with your question to support@git.eu. - No telemetry: the tool talks only to your source and your destination.
Stage Contract
The listing, config, secrets and staging formats are documented and versioned, so another stage
can be written in any language. See
doc/stage-contract.md
in the migratetogiteu repository.