Eintrag

Git, GitLab und VS Code unter Windows: Vom ersten Klon bis zum verlustfreien Repo-Umzug

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 commit tut, 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.

Git ist ein Programm auf dem PC, GitLab ein Server im Netz, VS Code ein Editor -- drei verschiedene Dinge mit drei verschiedenen Aufgaben.

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 push schickt 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

  1. Groß- und Kleinschreibung. Windows unterscheidet Readme.md und readme.md nicht, 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
    
  2. 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 unter C:\Projekte\ oder C:\Users\<Name>\src\ an.
  3. 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.

Vergleich: HTTPS mit Zugriffstoken gegenüber SSH mit Schlüsselpaar -- Vor- und Nachteile beider Zugangswege.

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 TokensAdd new token.

  • Token name: etwas Sprechendes, z. B. windows-arbeitsplatz
  • Expiration date: setzen (GitLab verlangt es in neueren Versionen)
  • Scopes: read_repository und write_repository reichen für Git. Nur wenn VS Code später auch Issues und Merge Requests anzeigen soll, brauchst du zusätzlich api und read_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 profileSSH KeysAdd 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 false finden. 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+PGitLab: Add Account → bei self-hosted die Instanz-URL eingeben → Token einfügen (Scopes api, read_user, read_repository, write_repository).

Sinnvolle Grundeinstellungen

Strg+Umschalt+PPreferences: 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:

Annotierte VS-Code-Oberfläche: Source-Control-Panel, Commit-Feld, Änderungsliste, Gutter-Marker, Branch-Anzeige und Sync-Knopf mit den zugehörigen git-Befehlen.

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/repositoryCreate 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+PGit: 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 .gitignore anlegen (siehe nächster Abschnitt). Sonst landen Passwörter, riesige Build-Ordner oder node_modules im 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:

Die vier Stationen: Arbeitsverzeichnis, Staging-Bereich, lokales Repository und GitLab -- mit den Befehlen, die dazwischen bewegen.

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 switch und git restore gibt es seit Git 2.23. Ältere Anleitungen zeigen für beides git 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 restore verworfen 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.

Drei Schichten eines GitLab-Projekts: Git-Daten reisen im Bundle mit, GitLab-Daten brauchen Export/Import, die Umgebung muss neu eingerichtet werden.

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:

  1. 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.
  2. 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.
  3. Prüfbar. git bundle verify sagt dir vor dem Import, ob die Datei vollständig und unbeschädigt ist.

Der Umzug in fünf Schritten: Spiegelkopie, Bundle erzeugen, Transport, prüfen, ins Ziel pushen.

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 age oder 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 --mirror bringt 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:

  1. Standard-Zweig setzen: Settings → Repository → Branch defaults – prüfen, ob dort main (bzw. euer Hauptzweig) steht.
  2. 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 Versions­unabhä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-files geprüft – LFS im Spiel?
  • Wiki vorhanden? Separat mitnehmen
  • Spiegelkopie mit git clone --mirror erstellt
  • Bundle erzeugt und mit git bundle verify geprüft
  • Prüfsumme vor und nach dem Transport verglichen
  • Leeres Zielprojekt in B angelegt (ohne README!)
  • git push --mirror ausgeführt
  • git ls-remote beider 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.
  • --mirror statt clone, wenn es vollständig sein soll. Der normale Klon ist zum Arbeiten gedacht, nicht zum Umziehen.
  • git bundle ist versionsunabhängig, GitLab-Export nicht. Deshalb wandert der Code per Bundle und die Diskussionen – wenn nötig – separat.
  • Verifizieren gehört dazu. git ls-remote auf 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

Dieser Eintrag ist vom Autor unter CC BY 4.0 lizensiert.