Saltar al contenido

Control de versiones de Git

· 17 minutos de lectura

Un control de videojuegos.

En mis inicios programando siempre escuchaba algo sobre el control de versiones, era algo que veía un poco complicado y donde no me quería meter porque estaba más enfocado en aprender otras cosas. Lo simplificaba como en qué estado se encuentra mi proyecto y usar versión alpha, beta, v1.0.0... Lo que ahora veo como una etiqueta que no se altera. Esto me llevó a realizar cambios sin justificación y a perder mucho trabajo que no guardé.

En algún momento al conocer Git me sentí igual que Flavio y por los recuerdos me dio a escribir sobre Git para no olvidar de que vale la pena saber más de unos comandos.

¿Qué es el control de versiones de Git?

El control de versiones es un sistema que guarda los cambios en el tiempo de uno o varios archivos. Se pueden revertir estos cambios a un estado anterior, lo que significa que si tenemos un problema, podemos comparar los cambios en el tiempo, ver quién modificó algo que pudiera causar el problema y poder cambiar el estado actual a uno donde no se presente el problema.

Git1 es un sistema de control de versiones que almacena la información como un flujo de snapshots de un sistema de archivos. Cada vez que hay un cambio o se guarda el estado del proyecto, Git toma una imagen de todos los archivos y crea una referencia a ese momento. En un futuro cambio, si un archivo no se modifica Git no vuelve a almacenar el archivo, sino que usa la referencia al momento anterior.

Git contempla tres estados:

  • modified: Cuando un archivo cambia, pero no se ha enviado a la base de datos local de Git.
  • staged: Cuando se añade un archivo modificado para ser enviado en el siguiente commit.
  • commited: Cuando los datos están almacenados en la base de datos local de Git.

Configuración de git

Para usar Git lo primero que se debe de hacer es instalarlo, se obtiene desde la página de descarga siguiendo los pasos según el sistema operativo que tengas.

Una vez instalado podremos usar en nuestra consola el comando git config --list para ver la configuración de Git. Para modificar los datos para el entorno global usamos git config --global <configuración> <valor>. Lo esencial a modificar tiene que ser el nombre y el email, que son las configuraciones que usan todos los commits.

git config --global user.name "MarcoMadera"
git config --global user.email "example@email.com"

git config --list
http.sslcainfo=C:/Program Files/Git/mingw64/ssl/certs/ca-bundle.crt
core.autocrlf=true
core.fscache=true
core.symlinks=false
pull.rebase=false
credential.helper=manager
core.editor="C:\Users\marco\AppData\Local\Programs\Microsoft VS Code\Code.exe" --wait
user.name=MarcoMadera
user.email=example@email.com

Uso básico

El flujo de trabajo en Git sigue el siguiente patrón:

  1. Haces modificaciones en tu directorio de trabajo controlado.
  2. Selecciona los cambios a añadir en el estado staged para ser enviados con el siguiente commit.
  3. Haces un commit, el cual toma todos los archivos en estado staged y almacena la snapshot en la base de datos de Git.

Para tener un directorio nuevo de trabajo controlado, en la terminal se dirige a la ruta del proyecto para inicializar el repositorio con el comando git init. Esto creará el archivo .git en la raíz del proyecto, donde se guardará la información de cada snapshot.

cd repositorios/gitpost/
git init
Initialized empty Git repository in C:/Users/marco/repositorios/gitpost/.git/

Para no ir a ciegas comando tras comandos podemos revisar el estado con git status. Nos informará dónde estamos, sobre el estado de los archivos, si se han modificado, agregado o eliminado.

git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use "git add <file>...". to include in what will be committed)
  README
nothing added to commit but untracked files present (use "git add" to track)

Para agregar archivos al stage usamos git add <archivo>, git add * o git add . agrega todos los archivos en el stage, git add *.<extensión> agrega los archivos con la extensión especificada y git add /<folder> agrega todos los archivos dentro de la carpeta especificada.

Si nos equivocamos, para eliminar archivos del stage sin borrarlo de nuestro directorio usamos git rm --cached <archivo>. El flag --cached hace que no se elimine de nuestro directorio, si no se agrega se eliminaría también de nuestro directorio de trabajo. Si olvidamos agregar el flag no está todo perdido, podemos recuperar el archivo con git restore <archivo>.

Para Ignorar archivos se crea un archivo .gitignore en la raíz del proyecto. Esto hace que todos los archivos que coincidan dentro de .gitignore no sean tomados en cuenta para ninguna acción con Git.

Tip: Puedes revisar la colección de archivos de .gitignore para ver ejemplos o usarlos en tus proyectos.

Ya teniendo todo lo que queramos para guardar los archivos usamos git commit. Esto abrirá el editor que definimos en la configuración para poner un mensaje descriptivo o igual lo podemos añadir en la consola con el flag -m. Para añadir archivos que ya habían estado en el stage usamos el flag -am que es la combinación de -a --add y -m --message.

git commit -m "<mensaje descriptivo>"
git commit -am "<mensaje descriptivo>"

Mostrar el historial de commits del repositorio usamos git log, el resultado de este es un poco feo, por lo que se puede hacer más bonito con el flag --pretty. git log solo mostrará por defecto el historial por debajo de la rama.

git log
commit ed3946555db4597294bae2014cfe996b88268bef (HEAD -> master, origin/master)
Author: MarcoMadera <example@email.com>
Date:   Mon Jul 6 17:09:50 2020 -0500

    hola mundo

commit e150e0079854fa6a5996db6ee692fc1377a1f2ff
Author: MarcoMadera <example@email.com>
Date:   Mon Jun 29 19:28:42 2020 -0500

    hello world

git log --oneline
65b5a12 (HEAD -> master, origin/master) hello world
fd14a30 hola mundo

git log --pretty=format: "%h | %cn | %cr | %s"
65b5a12 | MarcoMadera | 10 minutes ago | hello world
fd14a30 | MarcoMadera | 11 minutes ago | hola mundo

Para mostrar las diferencias entre un commit y otro de un archivo lo hacemos con el comando git diff. git diff muestra la diferencia por defecto de lo que has puesto en el stage y lo que vas a hacer commit. Muestra las líneas exactas que fueron añadidas o removidas. El comando puede ser selectivo usando el hash de cada commit a comparar.

Usar git diff no suele ser muy placentero de ver para archivos largos, se puede explorar el uso de git difftool para configurar una herramienta más gráfica para estos casos.

git diff commitA commitB

diff --git a/index.js b/index.js
index 5e1c309..ade1f58 100644
--- a/index.js
+++ b/index.js
@@ -1 +1 @@
-Hello World
+Hola Mundo

git difftool commitA commitB
Hello World       | Hola Mundo
~                 | ~

El Modelo de ramas

Se puede decir que Git tiene tres árboles donde se agrupan archivos. HEAD es el indicador del último commit realizado y de la rama actual. Index es el espacio donde se agregan/modifican/eliminan los archivos del antes mencionado stage antes de realizar un commit. Finalmente está el directorio de trabajo manejado como el Working tree

Al realizar el comando git init Git crea una rama por defecto que suele ser master. Esta no es una rama especial, es como cualquier otra con el detalle que es la inicial, la que Git crea por defecto. Nuestro proyecto puede seguir cualquier rama como principal en cualquier punto.

Cuando un commit es creado es enviado al HEAD, la rama actual, donde Git guarda la información de los cambios una única vez, lo demás son referencias con cambios, no se vuelve almacenar nada ya creado. Con esto Git permite crear copias de nuestro proyecto en un estado en formas de referencias y experimentar con ellas todo lo que queramos sin haber otro coste más que los nuevos cambios. Estos grupos de referencias en un estado son llamadas ramas que igual tendrá solamente un identificador propio.

Representación de ramas a1 a1a2 a2a1->a2 a3 a3a2->a3 b1 b1a2->b1 a4 a4a3->a4 a5 a5a4->a5 a6 a6a5->a6 a7 a7a6->a7 b2 b2b1->b2 c1 c1 b1->c1 b3 b3b2->b3 b4 b4b3->b4 b4->a7 c2 c2c1->c2 c2->b4

Ahora que tenemos una idea de lo que son las ramas pasemos al manejo de ellas. Para crear una rama usamos git branch <nombre de la rama>, esto creará un indicador llamado HEAD que apuntará a la rama en la que estamos para ubicarnos mejor, en este caso aún seguiríamos en la rama master. Para cambiar de ramas usamos git switch <nombre de la rama> o git checkout <nombre de la rama>, esto moverá el apuntador HEAD a la nueva línea en la que estaremos trabajando.

En algún punto las ramas pueden volver a unirse a la rama principal o a otra rama, como se muestra en la gráfica anterior. Todo lo que tienes que hacer es ir a la rama donde se van a hacer los cambios y usar git merge <nombre de rama>. Git creará una nueva snapshot de los cambios y un nuevo commit de referencia especial porque tendrá dos ancestros directos.

git checkout master
Switched to branch 'master'
git merge <nombre de rama>
Merge made by the 'recursive' strategy.
index.js | 1 +
1 file changed, 1 insertion(+)
create mode 100644 index.js

No todo es tan bonito siempre. Al momento de unir ramas, si cambias la misma parte del mismo archivo en las dos ramas que se han unido ocurrirá un conflicto, Git no podrá unirlas tan fácil, te indicará dónde está el conflicto y te pedirá que lo arregles.

git merge <nombre de rama>
Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
Automatic merge failed; fix conflicts and then commit the result.

Esto de unir ramas se puede volver un caos y nosotros podemos saber el trabajo que conlleva, pero ¿se lo queremos presentar al público así?. Al final lo que la gente ve es el resultado final y si alguien quiere ver como lo hiciste puede que quieras mostrar algo más coherente.

Se puede aplicar a lo que estás leyendo, duro tres días haciendo el artículo porque escojo el tema, investigo un poco, hago el borrador, creo las imágenes si las necesito, escojo los colores y reviso las faltas de ortografía. Lo primero que hice es hacer la gráfica de las ramas, pero si alguien ve el historial puede que no tenga sentido para nada.

El historial de commits es tal y lo que pasó, cambiar este historial sería cambiar el historial de cómo el proyecto fue construido. Los errores son parte de la historia del proyecto y son necesarias incluso para estudiar las soluciones.

El uso de git rebase puede ir en contra de esto porque coloca la base de una rama de un commit a otra rama diferente, pero en algunos casos puede ser útil, simplemente porque tal vez no quieras tener una rama completa para un cambio tan pequeño. Una vez ya acabado el trabajo de unir ambas ramas se puede eliminar la rama porque ahora está apuntando a la rama principal, por lo que ya no sería necesaria. Para eliminar ramas se hace con git branch -d <nombre de rama>.

git checkout <nombre de rama>
git rebase master
Successfully rebased and updated refs/heads/<nombre de rama>.

git checkout master
git merge <nombre de rama>
git branch -d <nombre de rama>

Repositorio Remoto

Todo lo que hemos estado trabajando es sobre el repositorio local de nuestro proyecto. Para que otras personas lo vean y colaboren con él, podemos usar los servicios remotos como GitHub, Bitbucket y GitLab; son servicios que permiten la gestión de proyectos y el seguimiento de trabajo con otros desarrolladores.

Para subir un repositorio local a uno remoto, tendremos que crear un repositorio en algunos de los servicios en el cual obtendremos una url del nuestro proyecto, con la que podremos utilizar git remote add <nombre identificador> <url>

Otra forma de obtener un repositorio es clonar un repositorio remoto con git clone <url> <nombre>. Con esto ya tendremos en nuestro directorio local una copia con la que podremos contribuir al proyecto o simplemente experimentar con su funcionamiento.

git clone https://github.com/MarcoMadera/Blog.git GitPost
Cloning into 'GitPost'...
remote: Enumerating objects: 64, done.
remote: Counting objects: 100% (64/64), done.
remote: Compressing objects: 100% (45/45), done.
remote: Total 64 (delta 32), reused 45 (delta 17), pack-reused 0
Unpacking objects: 100% (64/64), 74.53 KiB | 19.00 KiB/s, done.

Después de usar git clone, si usamos git remote veremos que tendremos origin, este es el nombre para identificar la url que Git le pone por defecto a los proyectos obtenidos por git clone. Esto sucede al igual que al inicializar un proyecto, Git por defecto crea la rama con nombre master. Se puede renombrar el identificador con el comando git remote rename <origin en este caso> <nuevo nombre>

Ahora que tenemos un repositorio remoto, puede que el contenido del remoto sea diferente por cambios de otros colaboradores. En nuestro repositorio local no se verán reflejados esos cambios. Para actualizar nuestro repositorio local podemos usar git pull. Trae los cambios generalmente del servidor al que se clonó y hace un merge automático en nuestro repositorio local. Para indicar otro servicio remoto y rama se usa git pull <remoto> <rama>

Cuando ya hemos hecho commit de los cambios que queremos compartir en nuestro repositorio, para actualizar el repositorio remoto usamos git push <remoto> <rama>, si alguien ya hizo un git push antes, nuestros cambios serán rechazados por lo que siempre es bueno hacer git pull antes.

Comandos de Git adicionales

Si no quieres escribir el comando completo cada vez, puedes fácilmente configurar un alias para cada comando. Los alias en Git nos permiten crear shortcuts, a través de git config --global alias.<atajo> comando.

git config --global alias.<atajo> "<comando>"
git config --global alias.st "status"
git git config --global alias.slog "log --pretty=format:'%h | %cn | %cr | %s'"

Si ya hicimos commit y olvidamos añadir un archivo o enmendar algún cambio podemos hacerlo con el comando git commit --amend --no-edit. Igualmente si nos equivocamos en la descripción de nuestro commit lo podemos arreglar con el mismo comando git commit --amend si lo invocamos después de haber ocurrido el error.

git commit -m "<mensaje>"
git add <archivo>
git git commit --amend

Git mantiene un log de dónde el HEAD y sus referencias han estado. Lo podemos ver con el comando git reflog y mostrar más a detalle la acción que se relizó en esa referecnai con el comando git show HEAD@{<número o referencia en días>}.

git reflog
2673d2d (HEAD -> master, origin/master) HEAD@{0}: merge newbranch: Merge made by the 'recursive' strategy.
4e85459 HEAD@{1}: checkout: moving from master to master
4e85459 HEAD@{2}: commit: Hola Mundo
8c821a7 HEAD@{3}: checkout: moving from newbranch to master

git show HEAD@{2}
commit ed3946555db4597294bae2014cfe996b88268bef
Author: MarcoMadera <example@email.com>
Date:   Mon Jul 6 17:09:50 2020 -0500

    Hola Mundo

diff --git a/index.js b/index.js
index 5e1c309..ade1f58 100644
--- a/index.js
+++ b/index.js
@@ -1 +1 @@
-Hello World
+Hola Mundo

Para crear una rama y cambiar directamente se usa el comando git checkout -b <nombre de rama> o git switch -c <nombre de rama>. Ahora si quieres volver a la rama anterior se puede usar git switch -.

Para cambiar el nombre de una rama se usa git branch --move <rama> <nuevo nombre>. Para enviar los cambios al repositorio remoto git push --set-upstream <remoto> <nuevo nombre>. Para eliminar la rama anterior del repositorio remoto utilizamos git push origin -d <rama>.

Cuando estamos trabajando, pero queremos cambiar de rama y no hacer un commit de un trabajo incompleto, usamos git stash. Guarda los commits en un estado diferente para poder recuperarlo después con git stash apply.

Git tiene una interfaz gráfica integrada que podemos utilizar con el comando gitk para ver el historial y git-gui donde puedes preparar los commits y experimentar lo visto.

Conclusión

Git nos proporciona una manera elegante de hacer el seguimiento de versiones, nos permite hacer un resguardo y hacer posible la colaboración entre varias personas teniendo espacios de trabajo separados como el local y el remoto. Git también se puede implementar en interfaces gráficas y tiene muchos comandos que no se han tocado en este artículo a fondo, por lo que te invito a investigar más sobre el tema.

Referencias

  1. Scott Chacon & Ben Straub Pro Git 2nd Edition (2014)


Recibirás actualizaciones del blog con temas de programación

Descubre más sitios indie