← Tous les articles

Git NFD vs NFC : corriger les fichiers fantômes sur Mac

Si git status sur votre Mac affiche un fichier comme Léa.txt dans la section supprimé et, juste en dessous, un fichier non suivi portant exactement le même nom, vous êtes face à un conflit NFD vs NFC sur les noms de fichiers. Le dépôt stocke le nom en Unicode composé (NFC, é en un seul point de code) et macOS fournit à Git la forme décomposée (NFD, un e suivi d'un accent combinant). Git compare des octets, donc pour lui ce sont deux fichiers différents. Côté Mac, la correction tient en un réglage, git config core.precomposeunicode true, et si des noms décomposés ont déjà été commités, il faut un git mv vers la forme composée. La suite de cet article explique comment confirmer le diagnostic et comment tenir le NFD à l'écart de vos scripts, de vos serveurs et de vos formulaires d'envoi.

Pourquoi Git voit deux Léa.txt : NFD vs NFC expliqué

Unicode autorise deux écritures pour certains caractères. é peut être un seul point de code (U+00E9, dit « précomposé ») ou deux points de code : e (U+0065) suivi d'un accent aigu combinant (U+0301). La première forme est le NFC, la seconde le NFD. À l'écran, les deux s'affichent exactement pareil. Il en va de même pour le ç de François (c plus une cédille combinante U+0327), le ô de Jérôme, le ë de Noël, et de façon bien plus visible pour le coréen (한글 peut se décomposer en jamo séparés) et pour les kana japonais avec dakuten (が devient か suivi de ゛).

Windows, Linux, la plupart des serveurs web et la plupart des éditeurs produisent du NFC. macOS a historiquement produit du NFD : l'ancien système de fichiers HFS+ normalisait chaque nom en forme décomposée, et même sous APFS, de nombreuses API macOS et le Finder renvoient encore des noms décomposés. Git, lui, ne normalise pas les chemins du tout. Il stocke les octets que lui donne l'arbre de travail et les compare avec ceux de l'index.

Le scénario classique est donc le suivant :

  1. Un collègue sous Linux ou Windows commite docs/Léa.txt en NFC.
  2. Vous clonez le dépôt sur un Mac. Git écrit le fichier avec le nom NFC, mais le système de fichiers ou l'outillage du Mac le lui renvoie en NFD.
  3. Git compare L\xC3\xA9a.txt (index) avec Le\xCC\x81a.txt (arbre de travail). Les octets ne correspondent pas, il signale donc le fichier NFC comme supprimé et le fichier NFD comme non suivi.

L'inverse se produit aussi : un utilisateur Mac commite des noms en NFD, et tout le monde sous Linux voit apparaître un « second » fichier impossible à supprimer depuis l'interface graphique sans que l'original ne disparaisse avec lui, ou bien un build qui ne trouve pas Léa.txt parce que le chemin écrit dans le code source est en NFC.

Vérifier quelle forme vous avez vraiment

Ne devinez pas. Regardez les octets.

Dans l'arbre de travail (macOS ou Linux) :

ls docs | grep -i lea | xxd
  • c3 a9 dans la sortie signifie NFC (é en un seul point de code).
  • 65 cc 81 signifie NFD (e suivi de U+0301).

Pour un ç, cherchez c3 a7 (NFC) ou 63 cc a7 (NFD).

Dans l'index, c'est-à-dire ce que Git a réellement commité :

git ls-files -z docs | xxd

Comparez les deux. Si l'index affiche c3 a9 et le listing du répertoire affiche 65 cc 81, le dépôt est sain et c'est votre Mac qui décompose. Si l'index lui-même contient des séquences cc 81 ou cc a7, des noms décomposés ont déjà été commités et doivent être réparés.

Pour repérer d'un coup tous les noms NFD déjà commités dans le dépôt :

git ls-files -z | perl -0ne 'print "$_\n" if /[\x{300}-\x{36f}\x{3099}\x{309a}\x{1100}-\x{11ff}]/'

Cette commande affiche les chemins contenant des diacritiques combinants latins, des dakuten/handakuten combinants japonais ou des jamo coréens conjoints.

Corriger le problème NFD vs NFC dans Git

Étape 1 : demander à Git sur macOS de recomposer les noms

git config --global core.precomposeunicode true

Avec ce réglage, Git convertit en NFC les noms NFD qu'il lit sur le système de fichiers avant de les comparer avec l'index et avant de commiter. Les versions récentes de Git sondent généralement le système de fichiers lors d'un git init ou d'un git clone et activent ce réglage automatiquement dans la configuration du dépôt, ce qui explique que le problème semble aller et venir d'une machine à l'autre. Le définir globalement supprime cette loterie. Après la modification, relancez git status : la paire fantôme supprimé/non suivi doit disparaître.

Ce réglage n'existe que dans la version macOS de Git. Sous Linux et Windows, il n'a aucun effet, ce qui n'est pas gênant puisque ces systèmes ne décomposent pas les noms.

Étape 2 : réparer les noms déjà commités en NFD

Si l'index contient des chemins décomposés, convertissez-les en NFC. Le nom doit changer octet par octet, donc procédez en deux mouvements (Git refuse un renommage vers un nom qu'il considère identique sur un système de fichiers insensible à la casse ou à la normalisation) :

# Construire la version NFC du chemin
nfc=$(printf '%s' 'docs/Léa.txt' | iconv -f utf-8-mac -t utf-8)

git mv 'docs/Léa.txt' docs/tmp-rename
git mv docs/tmp-rename "$nfc"
git commit -m "Normaliser le nom de fichier en NFC"

Remplacez le chemin littéral par le chemin décomposé issu de votre sortie git ls-files (le copier depuis le terminal conserve en général les octets décomposés). utf-8-mac est un nom d'encodage reconnu par le iconv de macOS ; sous Linux, utilisez Python à la place (section suivante) ou l'outil convmv : convmv -f utf-8 -t utf-8 --nfc --notest docs/Léa.txt.

S'il y a beaucoup de fichiers, bouclez sur la sortie de la commande de détection ci-dessus plutôt que de renommer à la main.

Étape 3 : éviter les surprises à la fusion

Une fois les noms cohérents, un fichier .gitattributes ne vous aidera pas ici (il gère les fins de ligne et les diffs, pas la normalisation des chemins). La protection repose donc sur le réglage de configuration plus une habitude : toute personne sur Mac doit avoir core.precomposeunicode activé. Un hook pre-commit qui exécute la commande de détection et échoue à la moindre correspondance constitue un filet de sécurité peu coûteux pour les équipes qui mélangent les systèmes d'exploitation.

Normaliser dans les scripts, sur les serveurs et à l'upload

Git n'est que l'endroit où le problème devient visible. Les mêmes noms NFD cassent les scripts de déploiement, les builds de sites statiques et les gestionnaires d'upload qui retrouvent les chemins par comparaison de chaînes. La règle est simple : normalisez en NFC à la frontière, là où un nom entre dans votre système.

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 (par exemple dans un gestionnaire d'upload, avant d'écrire sur le disque ou dans un stockage objet) :

const safeName = file.originalname.normalize("NFC");

En shell, lors d'une synchronisation d'un Mac vers un serveur Linux, laissez rsync faire la conversion pour que le serveur ne voie jamais de NFD :

rsync -av --iconv=utf-8-mac,utf-8 ./site/ user@serveur:/var/www/site/

Pour corriger en masse un répertoire déjà présent sur une machine Linux :

convmv -r -f utf-8 -t utf-8 --nfc --notest /var/www/uploads

Deux points à garder en tête. D'abord, un stockage objet comme S3 traite les clés comme des octets opaques : Léa.jpg envoyé depuis un Mac et Léa.jpg référencé dans votre base de données peuvent silencieusement être deux clés différentes. Normalisez avant de générer la clé. Ensuite, les navigateurs ne normalisent pas le nom de fichier dans un envoi multipart, donc un formulaire sur votre site reçoit exactement ce que le Mac de l'utilisateur a produit. La normalisation côté serveur est le seul endroit fiable pour l'intercepter.

Quand les fichiers viennent de quelqu'un hors de votre chaîne d'outils

Le code ci-dessus couvre votre propre pipeline. Il ne sert à rien quand une graphiste sur Mac dépose 300 images aux noms accentués dans un ticket, ou qu'un collègue coréen ou japonais envoie un ZIP dont les noms s'affichent sous Windows en lettres séparées (ㅎㅏㄴㄱㅡㄹ au lieu de 한글). Ces fichiers doivent être renommés avant d'approcher un dépôt, un CMS ou un serveur, et demander à l'expéditeur de « bien enregistrer en NFC » fonctionne rarement, puisqu'il ne voit aucune différence.

Pour ce cas, l'application web gratuite FilenameFixer fait la conversion sans aucun script : glissez les fichiers dans le navigateur et elle les renomme par lot de NFD vers NFC, ou donnez-lui l'archive ZIP (jusqu'à 200 Mo) et elle restaure les noms qu'elle contient. Elle convertit aussi du texte collé, ce qui est pratique pour corriger une liste de chemins dans un fichier de configuration. Le traitement s'effectue localement dans le navigateur, les fichiers eux-mêmes ne sont pas envoyés sur un serveur, ce qui compte quand il s'agit de documents clients. Si votre équipe envoie des fichiers via des formulaires web, l'extension Chrome corrige les noms automatiquement au moment de l'upload, ce qui comble le trou laissé par les navigateurs qui laissent passer le NFD tel quel.

Pour tout ce qui est déjà dans Git, restez sur core.precomposeunicode et git mv. Renommer des fichiers dans l'arbre de travail avec un outil externe sans le dire à Git recrée exactement la paire supprimé/non suivi par laquelle vous avez commencé.