Git, GitLab und VS Code unter Windows: Vom ersten Klon bis zum verlustfreien Repo-Umzug
Dieser Post richtet sich an alle, die mit Git, GitLab und VS Code noch nie gearbeitet haben. Es wird nichts vorausgesetzt: kein installiertes Git, keine Kommandozeilen-Erfahrung, kein Wissen darüber, was ein „Commit” ist. Wir fangen bei der Installation an und hören bei einer Aufgabe auf, an der auch Fortgeschrittene regelmäßig scheitern – ein komplettes Projekt von einer GitLab-Instanz auf eine andere umzuziehen, ohne dabei etwas zu verlieren, obwohl beide Instanzen unterschiedliche Versionen haben und sich womöglich nie im selben Netz sehen.
Die Ausgangslage über den ganzen Post hinweg:
- Du arbeitest an einem Windows-PC.
- Das GitLab läuft irgendwo im Netz – im Firmennetz, im Homelab, bei einem Hoster oder auf gitlab.com. Du hast entweder schon einen Zugang oder musst ihn einrichten.
- Du willst am Ende: Code sichern, mit anderen teilen, und im Notfall ein ganzes Projekt umziehen.
Wenn du bereits weißt, was
git committut, kannst du direkt zum Abschnitt Der große Umzug springen. Die Kapitel davor bauen aufeinander auf.
Erst die Begriffe, dann die Befehle
Der häufigste Anfängerfehler passiert vor dem ersten Befehl: Die drei Namen Git, GitLab und VS Code werden für dasselbe gehalten. Sie sind drei völlig verschiedene Dinge, und wer sie einmal sauber auseinanderhält, versteht später jede Fehlermeldung leichter.
Zusammengefasst:
| Name | Was es ist | Wo es läuft | Brauchst du es? |
|---|---|---|---|
| Git | Versionsverwaltung, ein Kommandozeilen-Programm | auf deinem PC | Ja, zwingend |
| GitLab | Webplattform, die Git-Repositories zentral aufbewahrt | auf einem Server | Ja, sobald du teilst oder sicherst |
| VS Code | Editor mit Git-Bedienoberfläche | auf deinem PC | Nein, aber sehr bequem |
Zwei Begriffe, die ab jetzt ständig vorkommen:
- Repository (kurz: Repo) – ein Projektordner, den Git überwacht. Erkennbar an einem versteckten Unterordner
.git. In diesem Unterordner liegt die komplette Historie. Löschst du ihn, ist aus dem Repo ein ganz normaler Ordner geworden. - Remote – eine Kopie deines Repos auf einem Server, üblicherweise „origin” genannt. Das ist dein GitLab-Projekt.
Wichtig: Git funktioniert vollständig ohne Internet. Du kannst wochenlang committen, ohne dass ein Server das je zu sehen bekommt. Erst
git pushschickt etwas nach draußen. Das ist keine Randnotiz, sondern der Grund, warum der Repo-Umzug später so unkompliziert ist.
Git unter Windows installieren
Installation über die Kommandozeile
Am schnellsten geht es mit dem in Windows enthaltenen Paketmanager winget. Öffne dazu PowerShell (Windows-Taste drücken, powershell tippen, Enter):
1
winget install --id Git.Git -e --source winget
Falls winget nicht verfügbar ist (sehr alte Windows-10-Installationen), lade den Installer von git-scm.com/download/win herunter und führe ihn aus.
Die Installer-Optionen – die vier, auf die es ankommt
Der grafische Installer stellt rund ein Dutzend Fragen. Die Voreinstellungen sind fast alle in Ordnung, aber vier Punkte solltest du bewusst entscheiden:
| Dialog | Empfehlung | Warum |
|---|---|---|
| Choosing the default editor | Visual Studio Code | Sonst landest du bei Merge-Konflikten im Editor vim und weißt nicht, wie du wieder rauskommst |
| Adjusting the name of the initial branch | Override → main |
Neuer Standard; GitLab erwartet ebenfalls main |
| Adjusting your PATH environment | Git from the command line and also from 3rd-party software | Nur so findet VS Code das Git |
| Configuring the line ending conversions | Checkout Windows-style, commit Unix-style | Dazu gleich mehr – das ist die klassische Windows-Falle |
Bei Choosing the SSH executable wählst du Use bundled OpenSSH, wenn du nur in Git Bash arbeitest – oder Use external OpenSSH, wenn du den Windows-eigenen SSH-Agenten nutzen willst. Wir kommen im SSH-Kapitel darauf zurück, weil genau hier später gern Verwirrung entsteht.
Prüfen, ob es geklappt hat
Schließe die PowerShell und öffne sie neu (damit der PATH neu eingelesen wird):
1
git --version
Die Ausgabe sieht ungefähr so aus:
1
git version 2.51.0.windows.1
Kommt stattdessen Der Begriff "git" wurde nicht als Name eines Cmdlets ... erkannt, wurde Git nicht in den PATH eingetragen – Installer erneut ausführen und die PATH-Option korrigieren.
Was du jetzt zusätzlich hast: Git Bash
Die Installation bringt neben git.exe auch eine Git Bash mit – ein Terminal, das sich wie eine Linux-Shell verhält. Du findest sie im Startmenü und im Kontextmenü des Explorers („Git Bash Here”). Der Unterschied in der Praxis:
| PowerShell | Git Bash | |
|---|---|---|
| Pfadtrenner | C:\Projekte\app |
/c/Projekte/app |
| Umgebungsvariable | $env:USERPROFILE |
$HOME |
| Kopieren in Zwischenablage | Set-Clipboard |
clip |
| Git-Befehle | funktionieren | funktionieren |
Beides ist richtig. Ich zeige in diesem Post überwiegend PowerShell, weil das die Standard-Shell unter Windows ist, und weise auf Unterschiede hin, wo sie eine Rolle spielen.
Git einmalig konfigurieren
Bevor du den ersten Commit machst, will Git wissen, wer du bist. Diese Angaben landen in jedem Commit und sind später nicht mehr sinnvoll änderbar.
1
2
git config --global user.name "Vorname Nachname"
git config --global user.email "[email protected]"
Nimm die E-Mail-Adresse, die auch in deinem GitLab-Konto hinterlegt ist. Nur dann ordnet GitLab die Commits deinem Profilbild und deinem Namen zu. Weicht sie ab, erscheinen die Commits als „unbekannter Autor” – funktioniert, sieht aber unschön aus.
Dazu kommen fünf Einstellungen, die dir unter Windows echte Probleme ersparen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Neue Repositories bekommen den Hauptzweig "main" statt "master"
git config --global init.defaultBranch main
# Zeilenenden: im Repo Unix (LF), im Arbeitsordner Windows (CRLF)
git config --global core.autocrlf true
# Pfade länger als 260 Zeichen erlauben (Windows-Altlast)
git config --global core.longpaths true
# VS Code als Editor, wartend (wichtig für Commit-Messages und Rebase)
git config --global core.editor "code --wait"
# Beim Zusammenführen keine Überraschungen: klassischer Merge
git config --global pull.rebase false
Kontrolle:
1
git config --global --list
Alle diese Werte landen in einer schlichten Textdatei unter C:\Users\<DeinName>\.gitconfig. Du darfst sie jederzeit mit einem Editor öffnen und ansehen – da ist keine Magie drin.
Exkurs: Warum Zeilenenden unter Windows wichtig sind
Windows beendet Textzeilen historisch mit zwei unsichtbaren Zeichen (CR + LF), Linux und macOS mit einem (LF). Für Git sind das unterschiedliche Dateiinhalte. Ohne Vorkehrung sieht ein Kollege unter Linux nach deinem Commit jede Zeile jeder Datei als geändert – obwohl du nur ein Wort korrigiert hast.
core.autocrlf true löst das für dich persönlich. Sauberer ist es, die Regel ins Projekt zu legen, dann gilt sie für alle Beteiligten unabhängig von deren Einstellungen. Dazu legst du im Projektordner eine Datei .gitattributes an:
1
2
3
4
5
6
7
8
9
* text=auto
*.sh text eol=lf
*.ps1 text eol=crlf
*.bat text eol=crlf
*.png binary
*.jpg binary
*.pdf binary
Übersetzt: Git entscheidet selbst, was Text ist, Shell-Skripte bekommen immer Unix-Zeilenenden (sonst laufen sie auf Linux-Servern nicht), Windows-Skripte immer Windows-Zeilenenden, und Bilder werden nie angefasst.
Siehst du beim ersten Commit die Warnung
LF will be replaced by CRLF the next time Git touches it– das ist keine Fehlermeldung, sondern genau diese Umwandlung bei der Arbeit. Du kannst sie ignorieren.
Drei weitere Windows-Eigenheiten, die später Zeit kosten
- Groß- und Kleinschreibung. Windows unterscheidet
Readme.mdundreadme.mdnicht, Git schon. Benennst du eine Datei nur in der Schreibweise um, merkt Git das nicht. Erzwingen kannst du es so:1
git mv --force readme.md README.md
- Projektordner nicht in OneDrive. Wenn OneDrive einen Ordner synchronisiert, in dem gleichzeitig Git arbeitet, entstehen Konflikte an Dateien im
.git-Ordner – mit teils kaputten Repositories. Lege Projekte lieber unterC:\Projekte\oderC:\Users\<Name>\src\an. - Virenscanner. Der Echtzeitschutz prüft jede der tausenden kleinen Dateien im
.git-Ordner. Bei großen Repos halbiert eine Ausnahme für den Projektordner die Wartezeiten spürbar.
Zugang zum GitLab einrichten
Jetzt braucht dein PC eine Möglichkeit, sich beim GitLab-Server auszuweisen. Es gibt zwei Wege, beide sind gleichwertig sicher.
Variante A: HTTPS mit Zugriffstoken
Der schnellere Weg, und in Netzen mit strenger Firewall oft der einzige.
Schritt 1 – Token in GitLab erzeugen. Melde dich im GitLab an. Klicke oben rechts auf dein Profilbild → Edit profile → in der linken Leiste Access Tokens → Add new token.
- Token name: etwas Sprechendes, z. B.
windows-arbeitsplatz - Expiration date: setzen (GitLab verlangt es in neueren Versionen)
- Scopes:
read_repositoryundwrite_repositoryreichen für Git. Nur wenn VS Code später auch Issues und Merge Requests anzeigen soll, brauchst du zusätzlichapiundread_user.
Klick auf Create personal access token. Der Token wird genau einmal angezeigt.
Kopiere ihn sofort in deinen Passwortmanager. Nach dem Neuladen der Seite ist er nicht mehr abrufbar – dann hilft nur, einen neuen zu erzeugen. Behandle ihn wie ein Passwort: Er kann alles, was du kannst.
Schritt 2 – Erste Verwendung. Beim ersten git clone oder git push über HTTPS öffnet sich ein Fenster des Git Credential Manager. Dort gibst du als Benutzername deinen GitLab-Benutzernamen und als Passwort den Token ein. Windows speichert ihn danach in der Anmeldeinformationsverwaltung (Systemsteuerung → Anmeldeinformationsverwaltung → Windows-Anmeldeinformationen).
Falls du dich vertippt hast und Git dich nicht mehr fragt, löschst du den gespeicherten Eintrag dort – oder per Befehl:
1
git credential-manager erase
Variante B: SSH mit Schlüsselpaar (empfohlen)
Etwas mehr Einrichtung, danach nie wieder Anmeldedaten.
Die Idee: Auf deinem PC entstehen zwei zusammengehörige Dateien. Der private Schlüssel bleibt für immer auf dem PC, der öffentliche wird ins GitLab hochgeladen. Der Server kann damit prüfen, ob du wirklich du bist, ohne dein Geheimnis je zu kennen.
Schritt 1 – Schlüsselpaar erzeugen:
1
ssh-keygen -t ed25519 -C "windows-arbeitsplatz-frank"
Drei Rückfragen folgen:
- Enter file in which to save the key – Enter drücken (Standardpfad
C:\Users\<Name>\.ssh\id_ed25519). - Enter passphrase – eine Passphrase vergeben. Sie schützt den Schlüssel, falls dein PC in falsche Hände gerät. Der SSH-Agent (Schritt 2) sorgt dafür, dass du sie nur einmal pro Sitzung eingibst.
- Enter same passphrase again – Wiederholung.
Ergebnis sind zwei Dateien:
| Datei | Inhalt | Regel |
|---|---|---|
id_ed25519 |
privater Schlüssel | verlässt nie diesen PC |
id_ed25519.pub |
öffentlicher Schlüssel | darf überall hin |
Schritt 2 – SSH-Agent starten, damit die Passphrase nicht bei jedem Push abgefragt wird. Dafür brauchst du einmalig eine PowerShell als Administrator:
1
2
Get-Service ssh-agent | Set-Service -StartupType Automatic
Start-Service ssh-agent
Danach als normaler Benutzer den Schlüssel hinzufügen:
1
ssh-add $env:USERPROFILE\.ssh\id_ed25519
Schritt 3 – öffentlichen Schlüssel in GitLab hinterlegen. In die Zwischenablage kopieren:
1
Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub | Set-Clipboard
Dann im GitLab: Profilbild → Edit profile → SSH Keys → Add new key. Inhalt einfügen, Titel vergeben, Usage type auf Authentication & Signing lassen, gegebenenfalls Ablaufdatum setzen → Add key.
Schritt 4 – testen:
1
ssh -T git@gitlab.beispiel.de
Beim allerersten Mal kommt eine Rückfrage nach dem Fingerabdruck des Servers – das ist normal, mit yes bestätigen. Danach sollte dort stehen:
1
Welcome to GitLab, @frank!
Erscheint stattdessen [email protected]: Permission denied (publickey), ist der öffentliche Schlüssel nicht (richtig) hinterlegt, oder es wird ein anderer Schlüssel angeboten als erwartet.
Die Stolperfalle: zwei verschiedene SSH-Programme
Unter Windows existieren zwei OpenSSH-Installationen nebeneinander: das mitgelieferte von Git for Windows (in C:\Program Files\Git\usr\bin\ssh.exe) und das Windows-eigene (C:\Windows\System32\OpenSSH\ssh.exe). Der Agent-Dienst aus Schritt 2 gehört zum Windows-eigenen. Nutzt Git das mitgelieferte, kennt es deinen entsperrten Schlüssel nicht – und fragt bei jedem Push nach der Passphrase.
Die Lösung ist eine Zeile:
1
git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"
Das ist der mit Abstand häufigste Grund für „SSH funktioniert im Terminal, aber Git fragt trotzdem nach dem Passwort”. Wenn dir das passiert – hier liegt es.
Sonderfälle self-hosted: eigener Port und eigenes Zertifikat
Läuft dein GitLab im Homelab oder Firmennetz, weicht es oft vom Standard ab. Beides regelst du einmalig.
SSH auf einem anderen Port (z. B. 2222, weil Port 22 schon vom Host belegt ist). Lege die Datei C:\Users\<Name>\.ssh\config an:
1
2
3
4
5
6
Host gitlab.beispiel.de
HostName gitlab.beispiel.de
User git
Port 2222
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
Danach funktionieren normale Adressen wie [email protected]:gruppe/app.git ohne weitere Angaben.
Selbst signiertes Zertifikat. Bei HTTPS meldet Git dann:
1
2
fatal: unable to access 'https://gitlab.beispiel.de/gruppe/app.git/':
SSL certificate problem: unable to get local issuer certificate
Die richtige Lösung ist, das CA-Zertifikat deiner Instanz bekannt zu machen – entweder im Windows-Zertifikatspeicher unter „Vertrauenswürdige Stammzertifizierungsstellen” (dann zusätzlich git config --global http.sslBackend schannel), oder als Datei:
1
git config --global http.sslCAInfo C:/certs/meine-ca.crt
Du wirst im Netz überall den Rat
git config --global http.sslVerify falsefinden. Damit schaltest du die Zertifikatsprüfung für alle Server ab und machst dich für Man-in-the-Middle-Angriffe angreifbar. Als kurzer Diagnoseschritt in Ordnung, als Dauerlösung nicht.
VS Code einrichten
Installation und Extensions
1
winget install --id Microsoft.VisualStudioCode -e
Zwei Erweiterungen lohnen sich sofort. Installieren über das Symbol Extensions in der linken Leiste (oder Strg+Umschalt+X):
| Extension | Kennung | Was sie bringt |
|---|---|---|
| GitLens | eamodio.gitlens |
Zeigt direkt hinter jeder Codezeile, wer sie wann und warum geändert hat |
| GitLab Workflow | gitlab.gitlab-workflow |
Issues, Merge Requests und Pipeline-Status ohne Browser |
Für GitLab Workflow meldest du dein Konto an: Strg+Umschalt+P → GitLab: Add Account → bei self-hosted die Instanz-URL eingeben → Token einfügen (Scopes api, read_user, read_repository, write_repository).
Sinnvolle Grundeinstellungen
Strg+Umschalt+P → Preferences: Open User Settings (JSON) und ergänzen:
1
2
3
4
5
6
7
8
{
"git.autofetch": true,
"git.confirmSync": false,
"git.enableSmartCommit": true,
"git.postCommitCommand": "none",
"files.eol": "\r\n",
"terminal.integrated.defaultProfile.windows": "Git Bash"
}
Was das bewirkt: VS Code holt regelmäßig im Hintergrund neue Commits vom Server (git.autofetch), fragt beim Synchronisieren nicht jedes Mal nach, und du kannst committen, ohne vorher Dateien einzeln auszuwählen. Die letzte Zeile macht Git Bash zum Standard-Terminal in VS Code – praktisch, weil damit alle Befehle aus diesem Post genauso funktionieren wie in Linux-Anleitungen.
Die Git-Oberfläche verstehen
VS Code erfindet nichts Eigenes – es ruft im Hintergrund dieselben git-Befehle auf, die du auch tippen könntest. Diese Übersicht zeigt, welcher Klick welchem Befehl entspricht:
Ein neues Projekt in GitLab anlegen
Jetzt wird es praktisch. Es gibt drei Wege – ich zeige alle drei, weil jeder in einer anderen Situation der richtige ist.
Weg 1: In GitLab anlegen, dann klonen (empfohlen für den Anfang)
Hier kann am wenigsten schiefgehen, weil das Projekt auf dem Server entsteht und dein PC nur eine Kopie holt.
In GitLab: Oben links auf Create new… (das Plus) → New project/repository → Create blank project.
- Project name: z. B.
mein-projekt(der URL-Pfad wird automatisch daraus abgeleitet) - Project URL: dein Benutzername oder eine Gruppe
- Visibility Level: Private, solange du nicht bewusst veröffentlichst
- Project Configuration: Initialize repository with a README ankreuzen
Nach Create project siehst du oben rechts die blaue Schaltfläche Code mit zwei Adressen:
1
2
SSH: [email protected]:frank/mein-projekt.git
HTTPS: https://gitlab.beispiel.de/frank/mein-projekt.git
Kopiere die Adresse, die zu deinem Zugang aus dem letzten Kapitel passt.
In VS Code: Strg+Umschalt+P → Git: Clone → Adresse einfügen → Zielordner wählen (z. B. C:\Projekte) → auf Nachfrage Open klicken.
Dasselbe im Terminal, falls du sehen willst, was passiert:
1
2
3
4
cd C:\Projekte
git clone git@gitlab.beispiel.de:frank/mein-projekt.git
cd mein-projekt
git status
git status ist der wichtigste Befehl überhaupt – er beantwortet immer die Frage „wo stehe ich gerade?”:
1
2
3
4
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
Weg 2: Vorhandener Ordner soll ins GitLab
Du hast schon einen Ordner mit Dateien und willst ihn nachträglich versionieren.
In GitLab legst du ein Projekt an wie oben – aber ohne das Häkchen bei „Initialize repository with a README”. Du brauchst ein wirklich leeres Projekt, sonst kollidieren gleich zwei Historien.
Auf dem PC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cd C:\Projekte\mein-vorhandenes-projekt
# 1. Ordner zum Repository machen (legt .git an)
git init
# 2. Alle Dateien zur Aufnahme vormerken
git add .
# 3. Ersten Commit erzeugen
git commit -m "Erster Commit: bestehender Projektstand"
# 4. Den Server als "origin" bekannt machen
git remote add origin git@gitlab.beispiel.de:frank/mein-projekt.git
# 5. Hochladen und die Verbindung merken (-u)
git push -u origin main
Vor Schritt 2 unbedingt eine
.gitignoreanlegen (siehe nächster Abschnitt). Sonst landen Passwörter, riesige Build-Ordner odernode_modulesim Repo – und sie wieder herauszubekommen ist deutlich mühsamer, als sie gar nicht erst aufzunehmen.
Meldet Git bei Schritt 5 error: src refspec main does not match any, heißt dein Hauptzweig noch master. Umbenennen:
1
2
git branch -M main
git push -u origin main
Weg 3: Push-to-create – das Projekt entsteht beim Hochladen
GitLab kann ein Projekt automatisch anlegen, wenn du in eine noch nicht existierende Adresse pushst. Das spart den Umweg über den Browser:
1
2
3
4
5
6
cd C:\Projekte\neues-projekt
git init
git add .
git commit -m "Erster Commit"
git remote add origin git@gitlab.beispiel.de:frank/neues-projekt.git
git push -u origin main
GitLab legt neues-projekt an – immer als privates Projekt. In der Ausgabe steht dann:
1
remote: The private project frank/neues-projekt was successfully created.
Das funktioniert in deinem persönlichen Namensraum immer, in Gruppen nur, wenn du dort mindestens Developer-Rechte hast.
Die .gitignore – was nicht ins Repo gehört
Diese Datei listet alles auf, was Git bewusst ignorieren soll. Sie gehört ins Projektwurzelverzeichnis und wird selbst mit eingecheckt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Windows-Systemdateien
Thumbs.db
desktop.ini
$RECYCLE.BIN/
# Editor
.vscode/*
!.vscode/settings.json
# Geheimnisse -- niemals ins Repo
.env
*.pem
*.key
credentials.json
# Build-Ergebnisse und Abhängigkeiten
node_modules/
dist/
build/
bin/
obj/
__pycache__/
*.pyc
# Große lokale Dateien
*.log
*.tmp
Zwei Faustregeln: Was erzeugt werden kann, gehört nicht ins Repo. Was geheim ist, erst recht nicht.
Ist ein Geheimnis schon committet und gepusht, reicht das Löschen im nächsten Commit nicht – es bleibt in der Historie lesbar. Dann gilt: Zugangsdaten sofort ändern (rotieren), und erst danach über eine Historien-Bereinigung nachdenken. Wie man Geheimnisse von vornherein aus dem Repo hält, steht im Post Secrets mit sops und age verschlüsseln.
Die wichtigsten Git-Befehle
Das Modell dahinter: vier Stationen
Fast alle Anfängerfehler kommen daher, dass unklar ist, wo eine Änderung gerade liegt. Git kennt vier Stationen, und jeder Befehl bewegt Dateien zwischen ihnen:
Der Staging-Bereich wirkt am Anfang überflüssig („warum kann ich nicht einfach speichern?”). Sein Sinn: Du hast heute an fünf Dateien gearbeitet, aber nur drei davon gehören inhaltlich zusammen. Mit git add wählst du diese drei aus und schreibst sie als einen sauberen, verständlichen Commit fest. Die anderen zwei bleiben für den nächsten.
Der tägliche Kreislauf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Was hat sich geändert?
git status
# Was genau wurde geändert? (zeilenweise)
git diff
# Auswählen: einzelne Datei ...
git add src/app.py
# ... oder alles im aktuellen Ordner
git add .
# Festschreiben mit einer Nachricht
git commit -m "Login-Formular validiert Eingaben"
# Hochladen zu GitLab
git push
Danach ein Blick in die Historie:
1
git log --oneline --graph --decorate -10
1
2
3
* 3f1a9c2 (HEAD -> main, origin/main) Login-Formular validiert Eingaben
* 8c40e17 README ergänzt
* a91d5f0 Erster Commit
Was die Zeile bedeutet: 3f1a9c2 ist die Kurzform der Commit-ID, HEAD -> main heißt „du stehst gerade auf dem Zweig main”, origin/main heißt „der Server ist auf demselben Stand”.
Nachschlagetabelle
| Befehl | Was er tut | Entsprechung in VS Code |
|---|---|---|
git init |
Ordner zum Repository machen | Initialize Repository |
git clone <url> |
Repo vom Server holen | Git: Clone |
git status |
Zeigt den aktuellen Zustand | Source-Control-Ansicht |
git add <datei> |
Datei für den Commit vormerken | + neben der Datei |
git commit -m "…" |
Änderungen festschreiben | Häkchen-Knopf |
git push |
Commits zum Server senden | Sync / Push |
git pull |
Commits vom Server holen und einbauen | Sync / Pull |
git fetch |
Nur holen, noch nicht einbauen | Fetch |
git log |
Historie ansehen | GitLens-Ansicht Commits |
git diff |
Unterschiede zeilenweise | Klick auf eine Datei |
git branch |
Zweige auflisten | Statusleiste unten links |
git switch -c <name> |
Neuen Zweig anlegen und hinwechseln | Create new branch |
git switch <name> |
Zweig wechseln | Checkout to… |
git merge <name> |
Zweig einarbeiten | Merge Branch… |
git restore <datei> |
Änderung verwerfen | Discard Changes |
git restore --staged <datei> |
Von der Bühne nehmen | − neben der Datei |
git stash |
Änderungen kurz beiseitelegen | Stash Changes |
git remote -v |
Zeigt, wohin gepusht wird | – |
git tag -a v1.0 -m "…" |
Version markieren | GitLens Tags |
git switchundgit restoregibt es seit Git 2.23. Ältere Anleitungen zeigen für beidesgit checkout– das funktioniert weiterhin, ist aber verwirrend, weil derselbe Befehl zwei völlig verschiedene Dinge tut.
Zweige (Branches) in zwei Minuten
Ein Zweig ist eine Parallelwelt deines Projekts. Du probierst etwas aus, ohne den funktionierenden Stand anzufassen.
1
2
3
4
5
6
7
8
9
# Neuen Zweig anlegen und hinwechseln
git switch -c feature/dunkles-design
# ... arbeiten, committen ...
git add .
git commit -m "Dunkles Design für die Startseite"
# Zweig zum ersten Mal hochladen
git push -u origin feature/dunkles-design
In GitLab erscheint danach automatisch ein Hinweis mit der Schaltfläche Create merge request. Ein Merge Request ist die Bitte „bitte übernehmt meinen Zweig in main” – inklusive Diskussion, Diff-Ansicht und Freigabe. Genau das ist der Mehrwert, den GitLab über reines Git hinaus bietet.
gitGraph
commit id: "Erster Commit"
commit id: "README"
branch feature/dunkles-design
commit id: "Farben angepasst"
commit id: "Kontraste geprüft"
checkout main
commit id: "Tippfehler"
merge feature/dunkles-design id: "Merge Request"
commit id: "Version 1.0"
Die tägliche Teamarbeit mit Merge Requests, Code Reviews und Branching-Strategien ist ein eigenes Thema – ausführlich im Post GitLab: Git-Workflow und Teamarbeit in der Praxis.
Die vier häufigsten „Oh nein”-Momente
| Situation | Rettung |
|---|---|
| Datei geändert, doch alles Quatsch | git restore datei.txt |
Falsche Datei mit git add vorgemerkt |
git restore --staged datei.txt |
| Commit-Nachricht vertippt (noch nicht gepusht) | git commit --amend -m "Neue Nachricht" |
| Letzten Commit rückgängig, Änderungen behalten | git reset --soft HEAD~1 |
Und die Universallösung, wenn gar nichts mehr klar ist: git reflog zeigt jede Bewegung der letzten Wochen, auch scheinbar verlorene Commits.
1
2
git reflog
git switch --detach 8c40e17 # zu einem alten Stand zurückspringen und ansehen
Solange du committet hast, ist praktisch nichts wirklich verloren. Git löscht unerreichbare Commits erst nach etwa 30 Tagen. Was hingegen wirklich weg ist: nicht committete Änderungen, die du mit
git restoreverworfen hast.
Der große Umzug: Projekt von GitLab A nach GitLab B
Jetzt die eigentliche Aufgabe. Das Szenario:
- GitLab A – die alte Instanz. Beispielsweise Version 16.11, im Firmennetz, wird abgeschaltet.
- GitLab B – die neue Instanz. Beispielsweise Version 18.2, beim Hoster oder im Homelab.
- Die beiden sehen sich möglicherweise nie: unterschiedliche Netze, keine Route, kein VPN. Dein Windows-PC ist der einzige Ort, an dem beide erreichbar sind – vielleicht nicht einmal gleichzeitig.
- Ziel: verlustfrei. Jeder Commit, jeder Zweig, jedes Tag, jeder Autor, jedes Datum.
Zuerst: Was heißt „verlustfrei” überhaupt?
Ein GitLab-Projekt besteht aus drei Schichten, die technisch nichts miteinander zu tun haben. Wer das übersieht, ist hinterher überrascht.
Für den Rest dieses Kapitels gilt: Wir bringen Schicht 1 garantiert vollständig hinüber, und ich zeige danach, wie du an Schicht 2 und 3 herangehst.
Warum der naheliegende Weg zu wenig transportiert
Der erste Reflex ist meistens dieser:
1
2
3
4
git clone git@gitlab-a:gruppe/app.git
cd app
git remote set-url origin git@gitlab-b:gruppe/app.git
git push
Das Ergebnis sieht gut aus – und ist unvollständig. Der Grund: Ein normales git clone legt lokal nur den Hauptzweig an. Alle anderen Zweige existieren zwar in deiner Kopie, aber nur als sogenannte Remote-Tracking-Referenzen (origin/feature-x). Und git push überträgt ausschließlich lokale Zweige.
Nachprüfen kannst du das jederzeit:
1
2
git branch # was lokal existiert -- meist nur: main
git branch -r # was auf dem Server liegt -- oft ein Dutzend Zweige
Dazu kommen zwei weitere Lücken: Tags wandern ohne --tags nicht mit, und wer irgendwann einmal mit --depth 1 geklont hat, besitzt gar keine vollständige Historie („shallow clone”), sondern nur den letzten Commit.
Die Merkregel: Für einen Umzug brauchst du keinen Klon zum Arbeiten, sondern eine Spiegelkopie. Der Unterschied heißt
--mirror.
Der Königsweg: git bundle
Ein Bundle ist eine einzige Datei, die ein komplettes Git-Repository enthält – alle Objekte, alle Referenzen. Git kann aus dieser Datei klonen, als wäre sie ein Server. Das hat drei Eigenschaften, die genau auf unser Problem passen:
- Versionsunabhängig. Im Bundle stecken reine Git-Objekte, kein GitLab-Format, keine Datenbank, kein Schema. Ob die Quelle GitLab 15 und das Ziel GitLab 18 ist, spielt keine Rolle – beide Seiten sprechen dasselbe Git.
- Offline-fähig. Die Datei kann per USB-Stick, Netzlaufwerk, Ticketsystem oder verschlüsselter Mail reisen. Zwischen A und B muss nie eine Verbindung bestehen.
- Prüfbar.
git bundle verifysagt dir vor dem Import, ob die Datei vollständig und unbeschädigt ist.
Schritt 1 – Spiegelkopie von GitLab A holen
1
2
cd C:\Umzug
git clone --mirror git@gitlab-a.beispiel.de:gruppe/app.git app.git
Was --mirror anders macht: Es holt alle Referenzen unverändert (refs/heads/*, refs/tags/* und weitere) und legt ein sogenanntes bare-Repository an – einen Ordner app.git ohne Arbeitsdateien, der nur die Git-Datenbank enthält. Genau das wollen wir.
Kontrolle, was drin ist:
1
2
3
4
5
cd app.git
git branch -a # alle Zweige
git tag # alle Tags
git rev-list --count --all # Gesamtzahl der Commits
git count-objects -vH # Größe des Repos
Notiere dir die Ausgabe von git rev-list --count --all – diese Zahl vergleichen wir am Ende.
Schritt 2 – Bundle erzeugen
1
git bundle create ..\app.bundle --all
--all bedeutet: alles, was unter refs/ liegt. Die Ausgabe zählt mit, wie viele Objekte verpackt werden:
1
2
3
4
Enumerating objects: 14382, done.
Counting objects: 100% (14382/14382), done.
Delta compression using up to 8 threads
Writing objects: 100% (14382/14382), 42.11 MiB | 38.42 MiB/s, done.
Schritt 3 – prüfen und Prüfsumme bilden
1
2
cd ..
git bundle verify .\app.bundle
1
2
3
4
5
6
The bundle contains these 14 refs:
3f1a9c2... refs/heads/main
8c40e17... refs/heads/feature/login
...
The bundle records a complete history.
app.bundle is okay
Die entscheidende Zeile ist The bundle records a complete history – das Bundle steht für sich allein und braucht kein Ursprungs-Repo.
Für den Transport bildest du eine Prüfsumme, damit du auf der anderen Seite weißt, dass die Datei heil angekommen ist:
1
Get-FileHash .\app.bundle -Algorithm SHA256
Notiere den Hash (oder schreibe ihn in eine Textdatei, die mitreist).
Schritt 4 – Datei transportieren
USB-Stick, Netzlaufwerk, SFTP, Mail – was in deiner Umgebung erlaubt ist. Nach dem Kopieren auf dem Zielrechner erneut die Prüfsumme bilden und vergleichen:
1
2
Get-FileHash .\app.bundle -Algorithm SHA256
git bundle verify .\app.bundle
Stimmen Hash und verify, ist ab hier nichts mehr kaputtzumachen.
Enthält das Repo vertrauliche Inhalte, verschlüssle die Datei für den Transport – ein Bundle ist unverschlüsselt lesbar. Unter Windows genügt ein Passwort-geschütztes 7-Zip-Archiv, sauberer ist
ageoder GPG.
Schritt 5 – Zielprojekt in GitLab B anlegen
Im GitLab B ein neues Projekt anlegen – leer, also ohne „Initialize repository with a README” und ohne Vorlage. Warum das wichtig ist: Ein initialisiertes Projekt hat bereits einen Commit. Der gehört zu einer anderen Historie und blockiert den Import.
Schritt 6 – aus dem Bundle klonen und ins Ziel pushen
1
2
3
4
git clone --mirror .\app.bundle app-neu.git
cd app-neu.git
git remote set-url origin git@gitlab-b.beispiel.de:gruppe/app.git
git push --mirror origin
Die Ausgabe listet alles auf, was ankommt:
1
2
3
4
5
* [new branch] main -> main
* [new branch] feature/login -> feature/login
* [new branch] release/2024 -> release/2024
* [new tag] v1.0.0 -> v1.0.0
* [new tag] v1.4.0 -> v1.4.0
git push --mirrorbringt das Ziel exakt auf den Stand der Quelle – und löscht dabei alles im Ziel, was in der Quelle nicht existiert. In ein frisch angelegtes, leeres Projekt ist das genau richtig. Auf ein Projekt, in dem schon jemand arbeitet, darfst du es nie anwenden.
Schritt 7 – nachprüfen, dass wirklich alles da ist
Der beste Test vergleicht die Referenzlisten beider Server direkt miteinander:
1
2
3
git ls-remote git@gitlab-a.beispiel.de:gruppe/app.git > C:\Umzug\refs-a.txt
git ls-remote git@gitlab-b.beispiel.de:gruppe/app.git > C:\Umzug\refs-b.txt
Compare-Object (Get-Content C:\Umzug\refs-a.txt) (Get-Content C:\Umzug\refs-b.txt)
Gibt Compare-Object keine Ausgabe zurück, sind beide Seiten Zeile für Zeile identisch – gleiche Commit-Hashes, gleiche Referenzen. Besser kann ein Nachweis nicht aussehen.
Zusätzlich im frisch geklonten Zielrepo:
1
2
git rev-list --count --all # muss der notierten Zahl aus Schritt 1 entsprechen
git fsck --full # prüft die Objektdatenbank auf Beschädigungen
Schritt 8 – Nacharbeiten in GitLab B
Zwei Dinge überträgt ein Mirror-Push nicht, weil sie GitLab-Einstellungen sind:
- Standard-Zweig setzen: Settings → Repository → Branch defaults – prüfen, ob dort
main(bzw. euer Hauptzweig) steht. - Geschützte Zweige und Tags: Settings → Repository → Protected branches / Protected tags neu einrichten.
Dazu kommt der Rest von Schicht 3: Mitglieder, CI/CD-Variablen, Deploy-Keys, Runner, Webhooks.
Variante für den Fall, dass beide Server erreichbar sind
Kannst du von deinem PC aus beide Instanzen erreichen, geht es ohne Zwischendatei – dieselbe Logik, zwei Befehle weniger:
1
2
3
git clone --mirror git@gitlab-a.beispiel.de:gruppe/app.git app.git
cd app.git
git push --mirror git@gitlab-b.beispiel.de:gruppe/app.git
Das ist bequemer, aber weniger robust: Bricht die Übertragung in der Mitte ab, ist der Zielzustand unklar und du fängst von vorn an. Beim Bundle-Weg hast du eine geprüfte Datei in der Hand, die du beliebig oft einspielen kannst.
sequenceDiagram
participant A as GitLab A (16.11)
participant PC as Windows-PC
participant D as app.bundle
participant B as GitLab B (18.2)
PC->>A: git clone --mirror
A-->>PC: alle Refs, alle Objekte
PC->>D: git bundle create --all
PC->>D: git bundle verify
Note over D: Transport per USB,<br/>Netzlaufwerk oder Mail
D->>PC: git clone --mirror app.bundle
PC->>B: git push --mirror
B-->>PC: new branch / new tag ...
PC->>A: git ls-remote
PC->>B: git ls-remote
Note over PC: Compare-Object:<br/>keine Abweichung = fertig
Sonderfall 1: Git LFS
Nutzt das Projekt Git LFS (Large File Storage, für große Binärdateien), steckt im Repo nur ein Verweis auf die eigentliche Datei – und das Bundle enthält folglich nur diese Verweise, nicht die Dateien. Prüfen kannst du das so:
1
git lfs ls-files
Kommt eine Liste zurück, brauchst du zwei zusätzliche Schritte. Im Spiegelklon von A:
1
git lfs fetch --all origin
Damit landen alle LFS-Objekte unter .git\lfs\objects. Dieser Ordner muss mit auf die Reise (einfach zusätzlich zum Bundle kopieren, in denselben Pfad des Zielklons legen). Nach dem Mirror-Push auf B dann:
1
git lfs push --all origin
Wenn nach dem Umzug alle großen Dateien plötzlich winzige Textdateien mit
oid sha256:…sind, ist genau dieser Schritt vergessen worden.
Sonderfall 2: Wiki und Snippets
Das Wiki eines GitLab-Projekts ist ein eigenes Git-Repository mit eigener Adresse – dieselbe URL, nur mit .wiki.git am Ende. Es zieht also exakt gleich um:
1
2
3
4
5
git clone --mirror git@gitlab-a.beispiel.de:gruppe/app.wiki.git app.wiki.git
cd app.wiki.git
git bundle create ..\app-wiki.bundle --all
# ... transportieren ...
git push --mirror git@gitlab-b.beispiel.de:gruppe/app.wiki.git
Damit der Push funktioniert, muss im Zielprojekt das Wiki-Feature aktiv sein (Settings → General → Visibility → Wiki). Für Snippets gilt dasselbe Prinzip, jeder Snippet ist ein eigenes kleines Repository.
Sonderfall 3: Issues, Merge Requests und der Versionsunterschied
Für Schicht 2 bietet GitLab einen eingebauten Export: Settings → General → Advanced → Export project. Du bekommst per Mail einen Link auf ein .tar.gz-Archiv, das Issues, Merge Requests, Kommentare, Meilensteine, Labels, Releases – und auch das Repository selbst enthält. Auf B importierst du es über New project → Import project → GitLab export.
Hier schlägt der Versionsunterschied allerdings voll durch:
| Konstellation | Export/Import | Konsequenz |
|---|---|---|
| A älter, B neuer (z. B. 16.11 → 18.2) | funktioniert in aller Regel | Erst versuchen, dann entscheiden |
| A und B gleiche Version | funktioniert | Der bequemste Fall |
| A neuer, B älter (z. B. 18.2 → 16.11) | schlägt fehl | Import in eine ältere Version wird nicht unterstützt |
Das ist der Kern der ganzen Sache: Der Export/Import-Weg ist versionsabhängig, der Bundle-Weg nicht. Deshalb lautet die robuste Strategie: Code immer per Bundle – und den Export nur zusätzlich versuchen, wenn du die Diskussionen ebenfalls brauchst. Scheitert er, ist der Code trotzdem längst sicher drüben.
Braucht ihr Issues aus einer neueren in eine ältere Instanz, bleibt der Weg über die API: Issues aus A auslesen und in B neu anlegen. Das lässt sich mit wenigen Zeilen skripten, verliert aber Zeitstempel und Autorenzuordnung – die neu angelegten Issues gehören dem API-Benutzer.
Welche Methode wann?
flowchart TD
Start["Projekt soll von A nach B"] --> Q1{"Werden Issues und<br/>Merge Requests gebraucht?"}
Q1 -->|Nein| Q2{"Sind beide Server<br/>gleichzeitig erreichbar?"}
Q1 -->|Ja| Q3{"Ist B gleich alt<br/>oder neuer als A?"}
Q2 -->|Ja| M1["clone --mirror<br/>+ push --mirror"]
Q2 -->|Nein| M2["git bundle<br/>Datei transportieren"]
Q3 -->|Ja| M3["GitLab Export/Import<br/>plus Bundle als Absicherung"]
Q3 -->|Nein| M4["Bundle für den Code<br/>+ Issues per API"]
M1 --> V["Verifizieren:<br/>git ls-remote vergleichen"]
M2 --> V
M3 --> V
M4 --> V
Im direkten Vergleich:
| Methode | Historie | Issues/MRs | Versionsunabhängig | Offline möglich | Aufwand |
|---|---|---|---|---|---|
| git bundle | vollständig | nein | ja | ja | gering |
| mirror clone + push | vollständig | nein | ja | nein | sehr gering |
| GitLab Export/Import | vollständig | ja | nein | ja (Datei) | mittel |
| Direct Transfer (Server zu Server) | vollständig | ja | eingeschränkt | nein | mittel |
| Ordner kopieren / ZIP | keine | nein | ja | ja | gering |
Die letzte Zeile steht bewusst da: Wer den Projektordner als ZIP kopiert und im neuen GitLab hochlädt, verliert die gesamte Historie und beginnt bei Commit eins. Das ist der Weg, den dieser Post verhindern soll.
Nach dem Umzug: die lokalen Klone aller Beteiligten umbiegen
Das Repo liegt jetzt auf B – aber auf jedem PC im Team zeigt origin noch nach A. Der Umbau ist ein Einzeiler pro Klon:
1
2
3
4
cd C:\Projekte\app
git remote set-url origin git@gitlab-b.beispiel.de:gruppe/app.git
git remote -v
git fetch
1
2
origin [email protected]:gruppe/app.git (fetch)
origin [email protected]:gruppe/app.git (push)
In VS Code passiert das automatisch mit, sobald du den Ordner neu öffnest – VS Code liest die Remote-Adresse jedes Mal frisch aus.
Zum Schluss: Das alte Projekt in GitLab A archivieren, nicht löschen. Settings → General → Advanced → Archive project macht es schreibgeschützt, aber lesbar. Falls doch etwas fehlt, hast du die Quelle noch. Löschen kannst du in ein paar Monaten immer noch.
Umzugs-Checkliste
- Repo-Größe und Commit-Anzahl auf A notiert (
git count-objects -vH,git rev-list --count --all) git lfs ls-filesgeprüft – LFS im Spiel?- Wiki vorhanden? Separat mitnehmen
- Spiegelkopie mit
git clone --mirrorerstellt - Bundle erzeugt und mit
git bundle verifygeprüft - Prüfsumme vor und nach dem Transport verglichen
- Leeres Zielprojekt in B angelegt (ohne README!)
git push --mirrorausgeführtgit ls-remotebeider Seiten verglichen – keine Abweichung- Standard-Zweig in B gesetzt
- Geschützte Zweige/Tags, CI/CD-Variablen, Deploy-Keys, Runner, Webhooks eingerichtet
- Mitglieder eingeladen
- Alle lokalen Klone auf die neue Adresse umgestellt
- Altes Projekt archiviert
Fehlermeldungen und was sie bedeuten
| Meldung | Ursache | Lösung |
|---|---|---|
fatal: not a git repository |
Du bist im falschen Ordner | cd in den Projektordner; git status prüft |
Permission denied (publickey) |
SSH-Schlüssel nicht hinterlegt oder falscher Schlüssel | ssh -T git@server testen; Schlüssel in GitLab prüfen |
| Git fragt trotz SSH-Agent nach der Passphrase | Git nutzt sein eigenes SSH | git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe" |
remote: HTTP Basic: Access denied |
Passwort statt Token verwendet, oder Token abgelaufen | Neues Token erzeugen, alte Anmeldedaten in Windows löschen |
SSL certificate problem |
Selbst signiertes Zertifikat | CA-Zertifikat hinterlegen (siehe oben), nicht sslVerify false |
Updates were rejected because the remote contains work |
Zielprojekt ist nicht leer | Erst git pull, oder für den Umzug ein wirklich leeres Projekt anlegen |
error: src refspec main does not match any |
Zweig heißt noch master oder es gibt keinen Commit |
git branch -M main, oder erst committen |
Filename too long |
Windows-Pfadgrenze | git config --global core.longpaths true |
LF will be replaced by CRLF |
Zeilenenden-Umwandlung | Keine Fehlermeldung – ignorieren oder .gitattributes anlegen |
pre-receive hook declined |
Push-Regel im Ziel (geschützter Zweig, Dateigrößenlimit) | Schutz kurz lockern oder Datei per LFS einbinden |
bundle is okay fehlt bei verify |
Datei beim Transport beschädigt | Neu kopieren, Prüfsumme vergleichen |
| Große Dateien nach Umzug sind Textschnipsel | LFS-Objekte nicht mitgezogen | git lfs fetch --all / git lfs push --all |
detached HEAD |
Du stehst auf einem Commit statt auf einem Zweig | git switch main – oder git switch -c neuer-zweig, um den Stand zu behalten |
Fazit
Der Weg von „ich habe noch nie mit Git gearbeitet” bis „ich ziehe ein komplettes Projekt zwischen zwei GitLab-Instanzen um” ist kürzer, als er aussieht – weil beides auf demselben kleinen Satz von Konzepten steht:
- Git lebt auf deinem PC. Der Server ist nur eine Kopie. Deshalb funktioniert der Umzug offline.
- Vier Stationen, ein paar Pfeile. Arbeitsverzeichnis, Bühne, lokales Repo, Remote – jeder Befehl bewegt etwas dazwischen.
- VS Code ist eine Bedienoberfläche, kein eigenes System. Jeder Klick ist ein
git-Befehl. --mirrorstattclone, wenn es vollständig sein soll. Der normale Klon ist zum Arbeiten gedacht, nicht zum Umziehen.git bundleist versionsunabhängig, GitLab-Export nicht. Deshalb wandert der Code per Bundle und die Diskussionen – wenn nötig – separat.- Verifizieren gehört dazu.
git ls-remoteauf beiden Seiten,Compare-Object– und du hast einen echten Nachweis statt eines guten Gefühls.
Wenn du für den Anfang nur eine Sache mitnimmst: Tippe git status, sooft du unsicher bist. Der Befehl ändert nichts, kostet nichts und beantwortet in vier Zeilen fast jede Frage, die während des Lernens auftaucht.
Weiterlesen
- GitLab: Git-Workflow und Teamarbeit in der Praxis – Branching-Strategien, Merge Requests, Code Reviews
- Git in der IDE: VS Code, Neovim und JetBrains im Vergleich – GitLens im Detail, Merge-Konflikte visuell lösen
- GitLab CI/CD: Pipelines, Runner und Automatisierung – automatisch bauen und ausliefern nach jedem Push
- Secrets mit sops und age verschlüsseln – Geheimnisse versionieren, ohne sie preiszugeben