Como seguramente sabrás, cada vez que en tu teclado pulsas la tecla ENTER
para cambiar de línea en tu código o en cualquier documento de texto, lo que ocurre es que se inserta un carácter de control que representa ese fin de línea. Estos caracteres de control no se ven, pero están ahí y ocupan memoria (o lo que es lo mismo, espacio en disco).
Como en muchas otras cosas, existen diferencias entre los sistemas operativos a la hora de tratar este tipo de caracteres de control. Y en concreto, Windows, macOs y Linux utilizan caracteres de control diferentes para hacerlo.
Si tienes acceso a dos sistemas de estos, es muy fácil de comprobar. Por ejemplo, abre el bloc de notas en Windows y escribe unas cuantas líneas de texto con un solo carácter en cada una de ellas. Ahora haz lo mismo en un editor de texto plano de Linux o de macOS, escribiendo exactamente las mismas líneas:
Si te fijas en sus tamaños, resaltados en rojo en la figura, el de Windows ocupa un poco más. En concreto 5 bytes más que el de macOS.
El motivo de esto es que en macOs y Linux se utiliza como carácter de control para cambio de línea un Avance de línea (Line Feed o LF
). En Windows, por otro lado, se utilizan dos caracteres: el mismo LF
y además, delante de éste, otro llamado Retorno de Carro (en inglés Carriage Return o CR
), es decir, Windows usa CRLF
, dos caracteres, como control para el cambio de línea. Por eso ocupa 1 byte más por línea y por tanto un 50% más en este caso extremo.
Nota friki #1: esta terminología viene de los tiempos de las máquinas de escribir 😯 El carro era un cilindro que agarraba el papel y que se movía en horizontal con cada letra y hacia arriba por cada línea. Cuando en una de estas máquinas terminabas de escribir una línea, le dabas un golpe al carro con la mano izquierda, que lo que hacía eran dos cosas: moverlo a la derecha (retorno de carro) y girar el carro un poco hacia arriba para subir el papel una línea (avance de línea). En este vídeo se pueden ver ambas cosas en acción: primero el avance de línea y luego el retorno de carro con un golpe más fuerte.
Nota friki #2: en los antiguos sistemas Mac "clásicos" que se comercializaron hasta 2001, el cambio de línea se hacía con un retorno de carro, CR
, solamente, al igual que ocurría con los clásicos ZX Spectrum o Commodore 64. El CRLF
de Windows se usaba también en sistemas clásicos como los Amstrad CPC, en OS/2, sistemas Atari y muchos otros. Y muchos sistemas embebidos o para mainframes todavía en uso en bancos y grandes aseguradoras utilizan sus propios caracteres de control para cambio de línea. De hecho, el estándar Unicode recoge hasta 8 caracteres de control que se deberían reconocer como terminadores de línea.
Si abrimos los 3 archivos (Windows, Mac clásico y Linux) con Notepad++, que es capaz de mostrar los caracteres de control, vemos las diferencias claramente:
Problemas de diferencias de archivos en Git debidos a retornos de carro
Cuando trabajamos en equipo con el sistema de control de código fuente Git, podemos llegar a tener problemas debido a estas diferencias.
Dependiendo de cómo tengamos configurado Git, si en el equipo tenemos a unos en Windows, a otros en macOS o en Linux, según quién edite en cada momento los archivos, pueden registrarse cambios que son debidos tan solo a estos elementos de control para cambio de línea.
Por regla general, los programas de comparación de archivos hacen caso omiso de los caracteres de control, por lo que no notaremos la diferencia. Pero sí que veremos archivos cambiados en los commits, y a la hora de copiar archivos a otro lado podremos sobrescribir algunos que realmente no han cambiado. Si, por ejemplo, copiamos archivos a otro servidor por la red local o por FTP y el programa de copiado decide, si debe sobrescribir o no, en función de si ha variado la fecha de modificación del archivo o su tamaño, podríamos copiar muchos más archivos de los necesarios solo por esa diferencia en los cambios de línea. O si hacemos despliegue continuo y se van a desplegar en el servidor mediante el propio Git, se podrían cambiar archivos sin querer sólo por estos cambios de línea.
En el asistente de instalación de Git en Windows y Linux se nos muestra una opción específica para controlar este comportamiento:
Se nos ofrecen 3 opciones que ya vienen bien explicadas en la propia pantalla pero que detallo a continuación en español:
- Checkout Windows-Style, commit Unix-style: esto implica que cuando nos traigamos los archivos desde el repositorio remoto, Git convertirá todos los cambios de línea en
CRLF
, es decir, en el estilo para Windows. Esto es lo que tendremos para trabajar localmente. A la hora de enviarlos al repositorio de nuevo, Git los convertirá de nuevo al estilo UNIX, es decir, a LF
.
- Checkout as is, commit Unix-style: como en la opción anterior, en este caso se convertirán todos los cambios de línea a
LF
al enviarlos al repositorio remoto y no se hará conversión alguna al traerse los archivos, es decir, se obtendrán tal cual están en el repositorio remoto.
- Checkout as-is, commit as-is: con esta opción, Git no hace cambio alguno sobre los cambios de línea.
La última opción es la peor de todas y la que no deberíamos elegir casi nunca. Si en el proyecto trabajan personas con diferentes sistemas o, si nosotros mismos trabajamos con más de un sistema operativo, acabaremos con el problema que describía antes, con varios tipos de cambios de línea mezclados.
Lo que deberíamos hacer es, marcar la opción 1 si trabajamos en Windows y la opción 2 si trabajamos en Linux o Mac. De este modo tendríamos siempre el tipo de cambio de línea apropiado para nuestro sistema, y al mismo tiempo se guardaría de manera homogénea en los repositorios remotos.
Puedes saber qué opción tienes establecida si abres una terminal y escribes:
git config --global core.autocrlf
Por ejemplo, en Windows yo tengo esta opción establecida a true
, que es lo correcto según hemos visto antes:
Y los compañeros que trabajen con macOS o Linux la deben establecer como input
, para que siempre se use LF
como cambio de línea.
Si tienes otro valor, puedes cambiarlo globalmente usando la misma instrucción que antes, pero indicando el valor que le vas a ajustar, por ejemplo:
git config --global core.autocrlf true
para Windows si no la tuvieras correctamente establecida, o con input
para Linux/macOS.
No dejar nada al azar: establecer por repositorio
El problema de lo anterior es que dejamos a la responsabilidad de cada desarrollador el establecer la opción correcta, lo cual es propenso a errores. Si no conocen la problemática o cuál es la opción correcta podríamos tener problemas.
La solución a esto es no dar la libertad de elegir y establecer esta opción directamente en todos los repositorios que tengamos. Para ello podemos hacer uso del archivo .gitattributes
.
Este archivo se crea en la raíz del repositorio y permite incluir configuración de Git que afectará tan solo al repositorio actual y que, por lo tanto, prevalece sobre lo que haya en la configuración global de Git.
En el caso que nos ocupa podemos establecer el modo en que gestionará Git cada archivo para los cambios de línea añadiendo patrones de archivos y un valor de configuración concreto.
Si queremos la configuración correcta equivalente a autocrlf
, lo que debemos hacer es escribir esto en ese archivo:
* text=auto
Esto significa que, para todos los archivos de texto, Git decidirá de manera automática qué hacer respecto de los cambios de línea, lo cual es equivalente a la situación ideal que mencionaba antes: que en Windows usará CRLF
y en macOS/Linux usará LF
. Además, al enviar los cambios a un repositorio remoto los homogeneizará a LF
.
Esto asegura consistencia para los cambios de línea entre todos los desarrolladores, sin importar en qué sistema operativo estén trabajando.
Es una gran idea tener un archivo .gitattributes
con configuraciones como esta como parte de las plantillas de proyectos que usemos. Como se mete bajo control de código, aparecerá en los repositorios locales de todos los desarrolladores.
Si queremos configurarlo de manera distinta para algún tipo de archivo concreto podemos hacerlo también. Por ejemplo, los archivos .sln
de Visual Studio que guardan información sobre los proyectos que forman parte de una solución son archivos de texto, y dado que Visual Studio sólo funciona en Windows, podríamos forzar a que siempre utilicen CRLF
como separador de línea escribiendo esto en el archivo .gitattributes
:
* text=auto
# Archivos de soluciones de Visual Studio
*.sln text eol=crlf
De este modo le estamos indicando a Git que lo archivos con la extensión .sln
son de texto, pero que debe utilizar siempre CRLF
para los cambios de línea, independientemente del sistema operativo en el que se clonen. Así evitamos que si lo abre un colaborador en macOS, cambie ese ajuste. No es que sea necesario, pero podríamos forzarlo así.
Arreglar un repositorio que no esté con cambios de línea homogéneos
Bien, con lo que hemos visto ya podemos crear una configuración homogénea para todo nuestro equipo (o para nosotros si trabajamos en varias máquinas con sistemas operativos diferentes).
Pero, ¿qué pasa si el mal ya está hecho y cuando hemos puesto este remedio en marcha ya tenemos centenares de archivos de código con diferentes estilos de cambio de línea?
Por suerte, Git ya tiene este problema contemplado también, así que podemos hacer lo siguiente:
git add --renormalize .
(¡Fíjate en el punto del final, es importante!)
Al hacer esto, Git recorrerá todos los archivos de contenido textual que haya en nuestro repositorio y se asegurará de que tienen los cambios de línea homogéneos, según la configuración que corresponda.
Tras hacerlo, tendremos en el área de preparación (staging) de Git todos los archivos que se han modificado y sólo tendríamos que hacer un commit
y un push
al repositorio remoto para persistir los cambios:
git commit -m "Homogeneizados los cambios de línea"
git push
Y listo.
Con estas técnicas sencillas podremos tener la seguridad de que no vamos a tener problemas por culpa de algo tan aparentemente inofensivo como unos cambios de línea.
¡Espero que te resulte útil!