Your Code. Your Rules.

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 start asks 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

With Homebrew:

brew install node git git-lfs

xcode-select --install also installs git.

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:

LaunchStep
1Checks Node.js and git, then asks its questions and writes migrate.config.json and migrate.secrets.json
2Checks the tokens and the destination, then writes migration.listing.json
3Checks again, then downloads every repository into staging/
4Checks 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:

  1. Copy to an instance, or local backup only (download, no destination).
  2. The source: GitHub, GitLab or another git.eu instance, and whose repositories (an organization, a user or a group).
  3. The destination: your instance address (https://<INSTANCE>.git.eu) and the push method (HTTPS or SSH).
  4. 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), start stops and prints the same steps as commands.
  • A gh output file or a URL list: run its generate command once by hand, then start takes 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/.html suffix gets rewritten, and every rewrite is listed in the dry-run. Collisions stop the run. Use --rename-map to 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:

ScopeUsed forEnvironmentSecrets file key
API accessCreating the repositoriesGITEU_DEST_API_TOKENdest.apiToken
Git accessPushing over HTTPSGITEU_DEST_GIT_PATdest.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, with GITEU_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:

ScopeUsed forEnvironmentSecrets file key
API accessListing your repositoriesGITEU_SOURCE_API_TOKENsources.giteu.apiToken
Git accessCloning over HTTPSGITEU_SOURCE_GIT_PATsources.giteu.gitPat

Clone over SSH instead with --source-method ssh and GITEU_SOURCE_SSH_KEY (sources.giteu.sshKey), or your SSH agent.

Other sources:

SourceTokenEnvironmentSecrets file key
GitHub, ghA GitHub token: private repositories, higher rate limitsGITHUB_TOKENsources.github.token
GitLabA GitLab token with read_api + read_repositoryGITLAB_TOKENsources.gitlab.token
GitLab over SSHA private key path, or your SSH agentGITLAB_SSH_KEYsources.gitlab.sshKey
URL listYour 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 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:

CauseMeaningFix
No git.eu API at this address (404)The URL is not a git.eu instanceGive your instance address in --dest-url
Token refused (401)The API token is wrong, expired or revokedCreate a new token on the destination
Token lacks the scope (403)The API token has no API accessCheck API access on the token
UnreachableNetwork error, or a typo in the host nameCheck 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. start and init write 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" }
  }
}
KeyOptionUse
destUrl--dest-urlThe destination instance, no default
dest.method--dest-methodPush over https (default) or ssh
sourceType--source-typeThe source of generate / run
sources.github.org / user--org / --userThe GitHub owner
sources.gitlab.user / group--user / --groupThe GitLab owner
sources.giteu.url / cloneMethod--source-url / --source-methodThe git.eu source and its clone method
sources.gitlab.url / cloneMethod--source-url / --source-methodThe GitLab server and its clone method
staging / logDir--staging / --log-dirWhere 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:

SourceOptionsReads
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 (or sources.gitlab.url, default https://gitlab.com) names the server.
  • --source-method https|ssh (or sources.gitlab.cloneMethod, default https) 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 internal visibility becomes private on git.eu.
  • Two projects with the same path in different subgroups are a name collision: generate reports it and writes no listing. Migrate each subgroup with its own --group acme/team (and a --rename-map when 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 (or sources.giteu.url) names the source instance.
  • --source-method https|ssh (or sources.giteu.cloneMethod, default https) picks how to clone, independent of --dest-method.
  • HTTPS clones need the source Git access token (GITEU_SOURCE_GIT_PAT): without it, download stops before any clone.
  • --no-forks and --no-archived do not apply; --visibility does.
  • import and run refuse 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 generate again with --overwrite.
  • A version 1 listing is refused: generate it again.
  • giteu-migrate generate --template listing, urls or rename-map writes 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 public or private after the URL to set it:
https://github.com/acme/tool.git
https://github.com/acme/docs.git public
  • Here tool is created private and docs public. Any other word is an error.
  • --visibility public|private|all only 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:

CommandDoesNeeds --dest-url
initWrites an example config, an owner-only secrets file and an example listingNo
doctor [listing]Checks the tools, the tokens, the destination and the listingFor the destination checks
generateStage 1: builds the listing from a sourceNo
download [listing]Stage 2: copies every listed repository into staging/ as a git bundleNo
import [listing]Stage 3: creates the repositories on the destination and pushes the bundlesYes
rungenerate, download and import in sequenceYes
status [listing]Shows each listed repository: staged, created, pushedYes
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-run shows the full plan and changes nothing.
  • -y skips the confirmation (scripts, CI).
  • import ends with the same report as start, and its retry command (--only <names>).
  • Write destUrl in migrate.config.json to 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.

OptionCommandsUse
--dest-url <url>doctor, import, run, statusThe destination instance
--dest-method <method>import, runhttps (token, default) or ssh (key)
--source-type <type>generate, rungithub, giteu, gitlab, gh-cli, urls
--listing <file>generate, runThe listing file to write
--overwriteinit, generate, runReplace a file that already exists
--redownloaddownload, runDownload again a repository already staged
--only / --skip <name,...>download, import, runProcess only / leave out these repositories
--force-pushimport, runPush even when the destination holds other history
--config / --secrets <file>allAnother config or secrets file

An option name that changed is refused as unknown, and its Fix: line names the new one:

Old nameNew 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, --archivedNothing: the default (--no-forks, --no-archived stay)
GITEU_API_TOKEN / GITEU_GIT_PATGITEU_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 --redownload stages again.
  • A destination repository holding only part of the refs (an interrupted push) is finished, no --force-push needed.
  • 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 .gitattributes file of any branch or tag holds filter=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. doctor shows the git-lfs version, or a not installed warning.
  • download checks 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.
  • import uploads the LFS objects before the branches and tags. An up to date repository shows lfs 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 large message, 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.