.gitattributes normalise les fins de ligne pour *.ipynb, *.lean, *.sh, .env, *.en.md, lake-manifest.json, les CSV de traduction, _quarto.yml, COURSE_CATALOG.generated.json, plusieurs README et twin_pairs.d/*.yaml — pas pour *.py. Avec core.autocrlf=false sur les machines du cluster, un outil qui écrit du CRLF dans un .py le commite tel quel, et le fichier devient « modifié des deux côtés » sur toute sa longueur au premier merge divergent.
Trouvé pendant la réparation P0 de #13951, dont c'était la cause racine.
Portée mesurée (main @ c90a0aee4)
Recensement des blobs de l'index, git cat-file blob HEAD:<path> sur les 1854 .py suivis :
| État |
Nombre |
| LF pur |
1845 |
| CRLF pur |
9 |
| mixte |
0 |
Les 9 :
MyIA.AI.Notebooks/QuantConnect/partner-course-quant-trading/examples/Crypto-MultiCanal/fix_ipynb_quotes.py
MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/argumentation_lib/_state_manager_plugin.py
scripts/lean/life_synthesize_sat.py
scripts/lean/tests/test_life_synthesize_sat.py
scripts/notebook_tools/check_lean4_wsl_repl.py
scripts/notebook_tools/tests/test_check_lean4_wsl_repl.py
scripts/tests/test_check_pr_perimeter.py
scripts/tests/test_gitleaks_allowlist.py
scripts/translation/tests/test_translation_full_mode.py
La convention de fait est déjà LF à 99,5 % : la règle ne changerait pas la pratique, elle la rendrait tenable.
Ce que ça a coûté, concrètement
Sur #13951, le premier commit a écrit scripts/tests/test_check_unaddressed_nits.py en CRLF (base 84d6a974d : CRLF 0 / LF 3754 → commit a17f1471a : CRLF 3801 / LF 3801) pendant que scripts/check_unaddressed_nits.py, resté en LF, fusionnait sans incident. Six jours plus tard le rebase produisait un conflit sur la totalité du fichier de test (marqueurs lignes 1 / 3845 / 7647), sans une seule collision sémantique. Réparation : rejeu des commits par instantané de contenu + normalisation LF, puis rebase — zéro conflit.
Le coût réel n'est pas le conflit lui-même mais ce qu'il cache : en résolvant à la main un conflit de 7600 lignes, on ne voit plus les vraies suppressions. Celle de #13951 (30 lignes de tests de main) n'a été trouvée qu'après avoir écarté le bruit CRLF.
Fix proposé
Effet : renormalisation des 9 fichiers ci-dessus, aucun autre. À faire en une PR dédiée qui committe la renormalisation (git add --renormalize -- '*.py') dans le même geste que la règle, sinon les 9 fichiers restent perpétuellement M dans les arbres de travail — c'est l'état décrit par #13493 pour .ipynb.
Point d'attention : la renormalisation touche 9 fichiers d'un coup ; la faire quand aucune PR ouverte ne les modifie (gh pr list --state open --json files sur ces chemins) pour ne pas déplacer le conflit ailleurs.
Note d'instrument (transposable)
git show <rev>:<path> applique la conversion working-tree et ment sur les fins de ligne : il rapportait 100 % CRLF à toutes les révisions, y compris là où od -c montrait du LF. Le recensement ci-dessus est pris avec git cat-file blob <rev>:<path>, qui rend les octets du blob. Toute mesure de fins de ligne faite avec git show est à refaire.
Voisins : #13493 (.ipynb perpétuellement M sous une règle eol=lf sans renormalisation committée), #11109 (mixte LF/CRLF sur _quarto.yml / JSON / CSV). Aucun des deux ne couvre *.py.
— lane myia-po-2026:CoursIA-2 (cycle c.884)
.gitattributesnormalise les fins de ligne pour*.ipynb,*.lean,*.sh,.env,*.en.md,lake-manifest.json, les CSV de traduction,_quarto.yml,COURSE_CATALOG.generated.json, plusieurs README ettwin_pairs.d/*.yaml— pas pour*.py. Aveccore.autocrlf=falsesur les machines du cluster, un outil qui écrit du CRLF dans un.pyle commite tel quel, et le fichier devient « modifié des deux côtés » sur toute sa longueur au premier merge divergent.Trouvé pendant la réparation P0 de #13951, dont c'était la cause racine.
Portée mesurée (
main@c90a0aee4)Recensement des blobs de l'index,
git cat-file blob HEAD:<path>sur les 1854.pysuivis :Les 9 :
La convention de fait est déjà LF à 99,5 % : la règle ne changerait pas la pratique, elle la rendrait tenable.
Ce que ça a coûté, concrètement
Sur #13951, le premier commit a écrit
scripts/tests/test_check_unaddressed_nits.pyen CRLF (base84d6a974d: CRLF 0 / LF 3754 → commita17f1471a: CRLF 3801 / LF 3801) pendant quescripts/check_unaddressed_nits.py, resté en LF, fusionnait sans incident. Six jours plus tard le rebase produisait un conflit sur la totalité du fichier de test (marqueurs lignes 1 / 3845 / 7647), sans une seule collision sémantique. Réparation : rejeu des commits par instantané de contenu + normalisation LF, puis rebase — zéro conflit.Le coût réel n'est pas le conflit lui-même mais ce qu'il cache : en résolvant à la main un conflit de 7600 lignes, on ne voit plus les vraies suppressions. Celle de #13951 (30 lignes de tests de
main) n'a été trouvée qu'après avoir écarté le bruit CRLF.Fix proposé
Effet : renormalisation des 9 fichiers ci-dessus, aucun autre. À faire en une PR dédiée qui committe la renormalisation (
git add --renormalize -- '*.py') dans le même geste que la règle, sinon les 9 fichiers restent perpétuellementMdans les arbres de travail — c'est l'état décrit par #13493 pour.ipynb.Point d'attention : la renormalisation touche 9 fichiers d'un coup ; la faire quand aucune PR ouverte ne les modifie (
gh pr list --state open --json filessur ces chemins) pour ne pas déplacer le conflit ailleurs.Note d'instrument (transposable)
git show <rev>:<path>applique la conversion working-tree et ment sur les fins de ligne : il rapportait 100 % CRLF à toutes les révisions, y compris là oùod -cmontrait du LF. Le recensement ci-dessus est pris avecgit cat-file blob <rev>:<path>, qui rend les octets du blob. Toute mesure de fins de ligne faite avecgit showest à refaire.Voisins : #13493 (
.ipynbperpétuellementMsous une règleeol=lfsans renormalisation committée), #11109 (mixte LF/CRLF sur_quarto.yml/ JSON / CSV). Aucun des deux ne couvre*.py.— lane
myia-po-2026:CoursIA-2(cycle c.884)