Crear parches con Git

Muchas veces pasa, especialmente cuando se trabaja en desarrollo de código, que modificamos software (por ej. algo tan simple como un script o varios archivos del código fuente de un programa) y queremos compartir esa modificación o solamente guardarla para tener esa "diferenciación" en la forma de un archivo de texto plano para más tarde aplicarla cuando el programa en el que nos basamos se actualice. Pues esa es la función que cumplen los parches.

En Wikipedia dice lo siguiente sobre los parches:

En informática, un parche consta de cambios que se aplican a un programa, para corregir errores, agregarle funcionalidad, actualizarlo, etc.

Pues hay varios métodos para crear parches, los más usados son diff y git diff. En este tutorial se enseñará el uso de git diff, por ser más completo.

Primer paso: crear los directorios

Este es un paso muy importante, que la mayoría de los tutoriales omiten, más adelante se explicará por qué.

Si se fijan bien, en Git, cada vez que se hace un commit se crea un parche, y cuando muestra un archivo modificado aparece el comando diff --git a/ruta/al/archivo/modificado.sh b/ruta/al/archivo/modificado.sh donde modificado.sh es, en este caso, un script que fue modificado (.❛ ᴗ ❛.)

Entonces, para modificar nuestro script, texto o código fuente primero hay que crear el directorio a y b

mkdir a b

En el directorio a pondremos el o los archivos sin modificar, y en el directorio b el modificado.

Segundo paso: crea el parche

Ejecuta:

git diff --no-prefix --no-index --no-renames --binary a b > parche.patch
  • --no-prefix: No mostrar ningún prefijo de origen o destino.
  • --no-index: Se usa para comparar las dos rutas dadas en el sistema de archivos.
  • --no-remanes: Desactiva la detección de cambio de nombre de un archivo.
  • --binary: Crea un diff binario que puede ser aplicado con git apply.

Ya tienen listo su parche. Sencillo ¿no?. Pues bien, ahora es la hora de probarlo.

Tercer paso: aplicar el parche

Una vez tenemos nuestro parche como archivo .diff o .patch (aunque en general se puede usar cualquier extensión), lo aplicaremos con patch o git apply dependiendo del caso.

  1. Solo texto plano: Si su parche únicamente modifica texto plano, como scripts, archivos de código fuente en C/C++, Python, Pascal, Javascript, PHP, HMTL, etc. entonces usaremos este comando:

    patch -p1 -i /ruta/del/parche.diff
    
  2. Con archivos binarios: Es decir, cosas como programas ejecutables ya compilados, imágenes PNG, JPEG, Gif, etc. que no sean texto plano. En general podrás identificar cuando se parcha un binario cuando en parche dice algo como "GIT binary patch". En este caso aplicaremos el parche de la siguiente manera:

    git apply -v /ruta/del/parche.diff
    

El problema con diff y no hacer directorios a y b

Ahora, regresando a lo que decía anteriormente sobre por qué esto es importante, se debe a que en muchas guías, wikis, etc. he encontrado que en vez de crear estos directorios, crean un archivo (por ej.) script.sh y script.sh.new y luego en base a eso ejecutan diff -u scripts.sh script.sh.new. Resulta que hay dos problemas en esto:

  • Al hacer eso, en el parche en vez de decir algo como diff --opciones a/ruta/al/archivo/modificado.sh b/ruta/al/archivo/modificado.sh dice (en este caso) diff --opciones script.sh script.sh.new, pero resulta que tu quieres parchar b/script.sh, no script.sh.new (porque dentro de b/ están los archivos modificados).
  • Si se usa diff, cuando se detecte un archivo que no existía originalmente en a/ (seguramente porque creaste uno en b/), no lo va a agregar en el parche, y si eliminaste uno dentro del árbol original, tampoco quitará dicho archivo.
  • diff no puede hacer parches de binarios.

Para que se entienda mejor, voy a ejemplificar cada caso con dos ejemplos. En el primero, crearé los archivos que puse de ejemplo (valga la redundancia) y usaré diff:

script.sh:

 #!/bin/bash
 echo "Hello world"

script.sh.new:

 #!/bin/sh
 echo "Hello world"
 echo "This is a patched file :D"

Ahora haremos lo que la mayoría de tutoriales de internet te dicen que hagas:

diff -u script.sh script.sh.new

Y me queda así:

--- script.sh   2018-03-16 15:52:49.887087539 -0300
+++ script.sh.new       2018-03-16 15:53:02.490420209 -0300
@@ -1,2 +1,3 @@
-#!/bin/bash
+#!/bin/sh
 echo "Hello world"
+echo "This is a patched file :D"

Todo aparentemente bien, pero ahora apliquemos dicho parche

$ diff -u script.sh script.sh.new | patch -p1 -i /dev/stdin
can't find file to patch at input line 3
Perhaps you used the wrong -p or --strip option?
The text leading up to this was:
--------------------------
|--- script.sh  2018-03-16 15:52:49.887087539 -0300
|+++ script.sh.new      2018-03-16 15:53:02.490420209 -0300
--------------------------
File to patch:

Falla siendo que estoy en el mismo directorio que script.sh{.new}, de modo que esto se corrige usando el hack de crear los directorios a/ y b/. Sin embargo, esto no resulve el punto 2 y 3. Vamos a por ello.

Supongamos que tenemos esto dentro de a/ y b/:

a: script.sh

b: archivo_binario.bin script.sh

Bien, ahora hagamos el parche con diff:

$ diff -ur a b
Sólo en b: archivo_binario.bin
diff -ur a/script.sh b/script.sh
--- a/script.sh 2018-03-16 15:37:27.513802777 -0300
+++ b/script.sh 2018-03-16 15:41:17.717123987 -0300
@@ -1,2 +1,3 @@
-#!/bin/bash
+#!/bin/sh
 echo "Hello world"
+echo "This is a patched file :D"

Y se cumple lo que decía en el punto 2, no te pone el archivo nuevo, te dice "Sólo en b" o si hay un fichero que está en a/ pero no en b/ (es decir, seguro que lo eliminaste de tu fork), te saldrá el mensaje "Sólo en a" en vez de eliminarlo o crearlo. Si aplicamos este parche solo afectará a los archivos de texto plano, y aunque hiciera bien su trabajo y creara este nuevo archivo no funcionaría porque archivo_binario.bin es un binario, el cual no está soportado por diff pero sí por git lo cual nos lleva al tercer punto. Mira lo que pasa si uso git en vez de diff:

$ git diff --no-prefix --no-index --no-renames --binary a b
diff --git b/archivo_binario.bin b/archivo_binario.bin
new file mode 100644
index 0000000000000000000000000000000000000000..1ce3c1c596d7a7f400b0cc89bda5a41eed2780c5
GIT binary patch
literal 73
pcmd-HXHZUIU{c}EWl|AfLZWk+R0P|Ad@#)bSHb~R0-{lr003gr3L5|b

literal 0
HcmV?d00001

diff --git a/script.sh b/script.sh
index da049c4..3d351f5 100644
--- a/script.sh
+++ b/script.sh
@@ -1,2 +1,3 @@
-#!/bin/bash
+#!/bin/sh
 echo "Hello world"
+echo "This is a patched file :D"

Ahora sí me consideró el archivo binario inexistente en a/ pero tangible en b/. Noten que en este caso particular, como ya expliqué anteriormente, al tratar con archivos binarios que solo git soporta (vean el mensaje "GIT binary patch") se debe usar git apply obligatoriamente. Pero les recomiendo usarlo solo cuando sea obligatorio, no siempre (en general no se usan muchos binarios en el software que es 100% libre, a no ser que se traten de casos como firmware para el kernel o librerías precompiladas, pero el software libre blobbeado suele tener binarios privativos en su código, aunque el hecho de que sea binario no significa que sea necesariamente privativo).

Si tienes dudas al respecto sobre el uso de diff y git diff o patch y git apply recuerda que puedes dejarlas en los comentarios, así como también leer sus manpages y consultar sus páginas web para más información.