Git: concilie dos carpetas con nombre duplicado (encasillado diferente) en Windows mientras preserva el historial

Recientemente descubrí que hay un par de carpetas en mi solución que tienen dos routes distintas en Git (GitHub muestra dos carpetas separadas), una es FooBar y la otra es Foobar . Esto se debe a que algunos files se registraron con el nombre de la carpeta anterior como su ruta, y algunos con la última.

Esto fue descubierto localmente (en Windows) al configurar Git para que no ignore el caso: git config core.ignorecase false

Intenté solucionar esto borrando toda la carpeta, confirmando, volviendo a agregar la carpeta y confirmando de nuevo. Esto solucionó el problema, pero los files que cambiaron sus routes perdieron su historial de Git . Ejecutar gitk contra la nueva ruta para estos files mostró solo el compromiso. Ejecutar a gitk contra su antiguo path reveló toda su historia.

Siguiente puñalada: use git mv para mover el file:

git mv Foobar/file.txt FooBar/file.txt

Esto produce el error:

fatal: destination exists, source=Foobar/file.txt, destination=FooBar/file.txt

Y si bash eliminar el file primero, por supuesto, Git se queja de que el file fuente no existe.

Luego descubrí que Git no se queja del destino ya existente si agrega -f al command mv . Sin embargo, después de cometer ese cambio de nombre, ¡ gitk muestra que la historia se cortó de todos modos!

Incluso intenté hacer el baile de tres pasos descrito aquí, pero esta era solo otra forma de hacer el -f . Mismo resultado.

Básicamente, solo quiero mover un file de Foobar/file.txt FooBar/file.txt a FooBar/file.txt en un sistema operativo insensible a mayúsculas / minúsculas de alguna manera, mientras se preserva el historial de Git. es posible?

No hay una solución simple al problema real.

En Git, los files no tienen historial. Los commits tienen historia, o más precisamente, commits son la historia. Esa es toda la historia que hay. Para que Git "siga" un file, como en el git log --follow <path> , Git examina los compromisos , uno a la vez, comparando cada compromiso con su confirmación principal.

Si una diferencia entre padre e hijo muestra que el padre contiene un file llamado parent/path/to/pfile y el hijo contiene un file llamado child/path/to/cfile y el contenido de estos dos files, en estos dos commits, es "suficientemente similar" (varias condiciones deben mantenerse aquí), entonces, en los "ojos" de Git, esa transición de padre a hijo representa un cambio de nombre de ese file. Entonces, en ese punto, git log --follow , que había estado buscando child/path/to/cfile , comienza a search parent/path/to/pfile .

Sin --follow , git log no hace esta operación especial de "encontrar un cambio de nombre" … y en general, Git cree que cualquier nombre de ruta con cualquier diferencia de nivel de bytes representa files diferentes. En otras palabras, no se produce el plegamiento de casos ni la normalización de UTF-8. Considere, por ejemplo, la palabra schön , que puede representarse como s c h ö n o s c h o combination- ¨ n . Podemos, en una caja de Linux, crear dos files diferentes usando estos dos nombres de estilo UTF-8 diferentes. Al ejecutar ls aparecerán dos files cuyo nombre aparece igual:

 $ cat umlaut.py import os p1 = u'sch\N{latin small letter o with diaeresis}n' p2 = u'scho\N{combining diaeresis}n' os.close(os.open(p1.encode('utf8'), os.O_CREAT, 0o666)) os.close(os.open(p2.encode('utf8'), os.O_CREAT, 0o666)) $ python umlaut.py $ ls schön schön umlaut.py 

Git está perfectamente feliz de almacenar ambos files, por separado. Sin embargo, MacOS se niega a permitir que ambos files coexistan, de la misma manera que Windows, y para el caso, también MacOS por defecto, se niega a permitir que tanto Foobar como FooBar coexistan.

Haga que Git almacene el file en nuevos commits bajo la nueva secuencia de bytes, y se conserve el historial, simplemente no es el historial que desea preservar. Pero la historia que ya está en el repository ya no es la historia que desea preservar.

En la práctica, probablemente debería cambiar el nombre del file en los ojos de Git, que no tiene ningún efecto en el nombre del file en los ojos de su sistema operativo; FooBar y Foobar son el mismo nombre aquí y siguen con las cosas. Su alternativa es reescribir toda la historia que se remonta en el time hasta el punto en que las parejas erróneas primero se agregaron al repository, copyndo (con ligeras modificaciones) cada compromiso "malo" a una confirmación "buena" nueva y mejorada. Pero esto significa que todos los que usan el repository pasen del "repo malo antiguo" al "repo bueno nuevo y mejorado".