NFD vs NFC Dateinamen in Git: Umlaut-Fehler am Mac beheben
Wenn git status auf deinem Mac eine Datei wie Müller.txt als gelöscht meldet und direkt darunter eine unversionierte (untracked) Datei mit exakt demselben Namen auftaucht, hast du einen Konflikt zwischen NFD und NFC Dateinamen. Das Repository speichert den Namen in der zusammengesetzten Unicode-Form (NFC, ü als ein Codepunkt), macOS liefert Git aber die zerlegte Form (NFD, u plus kombinierende Trema-Punkte). Git vergleicht Bytes, also sind das für Git zwei verschiedene Dateien. Auf Mac-Seite reicht eine Einstellung, git config core.precomposeunicode true; wurden bereits zerlegte Namen committet, kommt ein git mv auf die zusammengesetzte Form dazu. Der Rest dieses Artikels zeigt, wie du die Diagnose sicher bestätigst und wie du NFD aus Skripten, Servern und Uploads heraushältst.
Warum Git zwei Müller.txt sieht: NFD vs NFC Dateinamen
Unicode erlaubt es, manche Zeichen auf zwei Arten zu schreiben. ü kann ein einzelner Codepunkt sein (U+00FC, „vorkomponiert“) oder zwei Codepunkte: u (U+0075) gefolgt von einem kombinierenden Trema (U+0308). Die erste Variante ist die NFC-Form, die zweite NFD. Auf dem Bildschirm sehen beide identisch aus. Dasselbe gilt für ä, ö, é in René, ñ – und noch sichtbarer für Koreanisch (한글 zerfällt in einzelne Jamo) und japanische Kana mit Dakuten (が wird zu か plus ゛). Das ß ist übrigens nicht betroffen, es hat keine zerlegte Form.
Windows, Linux, die meisten Webserver und die meisten Editoren erzeugen NFC. macOS hat historisch NFD erzeugt: Das alte Dateisystem HFS+ hat jeden Namen in die zerlegte Form normalisiert, und auch unter APFS liefern viele macOS-APIs und der Finder weiterhin zerlegte Namen. Git normalisiert Pfade überhaupt nicht. Es speichert die Bytes, die das Arbeitsverzeichnis liefert, und vergleicht sie mit dem Index.
Der typische Ablauf sieht so aus:
- Ein Kollege unter Linux oder Windows committet
docs/Müller.txtin NFC. - Du klonst auf dem Mac. Git schreibt die Datei mit dem NFC-Namen, aber das Dateisystem oder die Mac-Werkzeuge melden sie als NFD zurück.
- Git vergleicht
M\xC3\xBCller.txt(Index) mitMu\xCC\x88ller.txt(Arbeitsverzeichnis). Keine Übereinstimmung, also gilt die NFC-Datei als gelöscht und die NFD-Datei als untracked.
Das Gegenteil passiert ebenfalls: Ein Mac-Nutzer committet NFD-Namen, und alle unter Linux sehen eine „zweite“ Datei, die sie über die grafische Oberfläche nicht löschen können, ohne dass das Original mitgeht – oder ein Build findet Müller.txt nicht, weil der Pfad im Quellcode in NFC steht.
Prüfen, welche Form du wirklich hast
Nicht raten, sondern die Bytes ansehen.
Im Arbeitsverzeichnis (macOS oder Linux):
ls docs | grep -i m.ller | xxd
c3 bcin der Ausgabe bedeutet NFC (üals ein Codepunkt).75 cc 88bedeutet NFD (ugefolgt von U+0308).
Für ä lauten die Werte c3 a4 gegenüber 61 cc 88, für ö c3 b6 gegenüber 6f cc 88.
Im Index, also dem, was Git committet hat:
git ls-files -z docs | xxd
Vergleiche beides. Zeigt der Index c3 bc und die Verzeichnisliste 75 cc 88, ist das Repository in Ordnung und dein Mac ist derjenige, der zerlegt. Enthält der Index selbst cc 88-Sequenzen, wurden zerlegte Namen bereits committet und müssen repariert werden.
Schnellere Prüfung über das ganze Repository auf bereits committete NFD-Namen:
git ls-files -z | perl -0ne 'print "$_\n" if /[\x{300}-\x{36f}\x{3099}\x{309a}\x{1100}-\x{11ff}]/'
Das gibt alle Pfade aus, die lateinische kombinierende Zeichen, japanische kombinierende Dakuten/Handakuten oder koreanische Jamo enthalten.
Das Problem in Git beheben
Schritt 1: Git auf macOS anweisen, Namen wieder zusammenzusetzen
git config --global core.precomposeunicode true
Mit dieser Einstellung wandelt Git die vom Dateisystem gelesenen NFD-Namen zurück in NFC, bevor es sie mit dem Index vergleicht und bevor es committet. Neuere Git-Versionen prüfen das Dateisystem meist schon bei git init oder git clone und setzen die Option automatisch in der Repository-Konfiguration – deshalb wirkt das Problem oft so, als käme und ginge es je nach Rechner. Die globale Einstellung beendet dieses Glücksspiel. Danach noch einmal git status ausführen; das Phantom-Paar aus gelöscht und untracked sollte verschwunden sein.
Diese Option existiert nur im macOS-Build von Git. Unter Linux und Windows tut sie nichts, was in Ordnung ist, weil diese Systeme Namen nicht zerlegen.
Schritt 2: bereits in NFD committete Namen reparieren
Enthält der Index zerlegte Pfade, änderst du sie auf NFC. Der Name muss sich Byte für Byte ändern, deshalb geht das in zwei Zügen (Git weigert sich, auf einen Namen umzubenennen, den es auf einem Dateisystem ohne Unterscheidung von Groß-/Kleinschreibung oder Normalisierung für identisch hält):
# NFC-Version des Pfads erzeugen
nfc=$(printf '%s' 'docs/Müller.txt' | iconv -f utf-8-mac -t utf-8)
git mv 'docs/Müller.txt' docs/tmp-rename
git mv docs/tmp-rename "$nfc"
git commit -m "Dateinamen auf NFC normalisieren"
Ersetze den Pfad im Beispiel durch den zerlegten Pfad aus deiner git ls-files-Ausgabe (Kopieren aus dem Terminal behält die zerlegten Bytes meist bei). utf-8-mac ist ein Encoding-Name, den das iconv von macOS versteht; unter Linux nimmst du stattdessen Python (nächster Abschnitt) oder das Werkzeug convmv: convmv -f utf-8 -t utf-8 --nfc --notest docs/Müller.txt.
Bei vielen Dateien lohnt sich eine Schleife über die Ausgabe des Erkennungsbefehls von oben, statt von Hand umzubenennen.
Schritt 3: Überraschungen beim Merge vermeiden
Sobald die Namen einheitlich sind, hilft eine .gitattributes-Datei hier nicht weiter (sie regelt Zeilenenden und Diffs, nicht die Normalisierung von Pfaden). Der Schutz besteht also aus der Konfigurationsoption plus einer Gewohnheit: Wer am Mac arbeitet, sollte core.precomposeunicode aktiviert haben. Ein Pre-Commit-Hook, der den Einzeiler zur Erkennung ausführt und bei jedem Treffer abbricht, ist ein günstiges Sicherheitsnetz für Teams mit gemischten Betriebssystemen.
In Skripten, auf Servern und bei Datei-Uploads normalisieren
Git ist nur der Ort, an dem das Problem sichtbar wird. Dieselben NFD-Namen bringen Deploy-Skripte, Builds statischer Websites und Upload-Handler zum Stolpern, die Pfade per String-Vergleich nachschlagen. Die Regel ist einfach: An der Grenze, an der ein Name in dein System eintritt, auf NFC normalisieren.
Python:
import unicodedata, os
for name in os.listdir("uploads"):
nfc = unicodedata.normalize("NFC", name)
if nfc != name:
os.rename(os.path.join("uploads", name), os.path.join("uploads", nfc))
Node.js (zum Beispiel in einem Upload-Handler, bevor auf die Festplatte oder in den Objektspeicher geschrieben wird):
const safeName = file.originalname.normalize("NFC");
Shell, beim Abgleich von einem Mac auf einen Linux-Server: Lass rsync die Umwandlung übernehmen, damit der Server nie NFD zu sehen bekommt:
rsync -av --iconv=utf-8-mac,utf-8 ./site/ user@server:/var/www/site/
Ein Verzeichnis, das bereits auf einer Linux-Maschine liegt, in einem Rutsch korrigieren:
convmv -r -f utf-8 -t utf-8 --nfc --notest /var/www/uploads
Zwei Dinge sollte man wissen. Erstens: Objektspeicher wie S3 behandeln Schlüssel als reine Bytes, also können Müller.jpg vom Mac hochgeladen und Müller.jpg aus deiner Datenbank referenziert unbemerkt zwei verschiedene Schlüssel sein; normalisiere, bevor du den Schlüssel erzeugst. Zweitens: Browser normalisieren den Dateinamen in einem Multipart-Upload nicht, ein Formular auf deiner Website bekommt also genau das, was der Mac des Nutzers erzeugt hat. Die serverseitige Normalisierung ist die einzige zuverlässige Stelle, um das abzufangen.
Wenn die Dateien von außerhalb deiner Toolchain kommen
Der Code oben deckt deine eigene Pipeline ab. Er hilft nicht, wenn eine Designerin am Mac 300 Bilder mit Umlauten im Namen in ein Ticket hängt, oder wenn ein koreanischer oder japanischer Kollege ein ZIP schickt, dessen Namen unter Windows als getrennte Buchstaben erscheinen (ㅎㅏㄴㄱㅡㄹ statt 한글). Diese Dateien müssen umbenannt werden, bevor sie in die Nähe eines Repositorys, eines CMS oder eines Servers kommen – und die Bitte an den Absender, „bitte in NFC speichern“, funktioniert selten, weil er den Unterschied gar nicht sehen kann.
Für diesen Fall erledigt die kostenlose FilenameFixer-Web-App die Umwandlung ohne jedes Skript: Dateien in den Browser ziehen, und sie werden im Stapel von NFD nach NFC umbenannt; alternativ das ZIP-Archiv (bis 200 MB) übergeben, und die Namen darin werden wiederhergestellt. Auch eingefügter Text lässt sich umwandeln, was praktisch ist, um eine Liste von Pfaden in einer Konfigurationsdatei zu korrigieren. Die Verarbeitung passiert lokal im Browser, die Dateien selbst werden nicht auf einen Server hochgeladen – das zählt, wenn es sich um Kundenmaterial handelt. Lädt dein Team über Webformulare hoch, korrigiert die Chrome-Erweiterung die Dateinamen automatisch beim Upload und schließt damit die Lücke, die Browser lassen, wenn sie NFD unverändert durchreichen.
Für alles, was bereits in Git liegt, bleibst du bei core.precomposeunicode und git mv. Dateien im Arbeitsverzeichnis mit einem externen Werkzeug umzubenennen, ohne Git Bescheid zu geben, erzeugt nur wieder dasselbe Paar aus gelöscht und untracked, mit dem du angefangen hast.