Tabla de Contenidos
Commits
Sección perteneciente al curso Control de versiones con Git Avanzado.
Introducción
Una de las unidades más importantes. Veremos cómo preparar un commit. Un commit no es un backup sino un momento concreto en la historia de cambios de nuestro proyecto. Una de las características más potentes de Git es que nos permite preparar el commit, qué ficheros van a ir a un commit y cuáles no.
También veremos cómo ver commits del pasado y cómo deshacerlos.
Preparación de un commit por etapas
Trabajando con el stage
Tanto para añadir nuevos archivos al repositorio (empezar a versionar) como para añadir modificaciones en archivos ya existentes, tendremos que “subirlos al stage”:
git add <RUTA>
Para inicializar un repositorio, es decir, que Git pueda empezar a trabajar con él, ejecutamos dentro del directorio el comando git init. A partir de entonces ya podremos usar git.
Cuando queremos “subir al stage” un archivo:
git add archivo
Si queremos añadir más de un archivo:
git add archivo1 archivo2 archivo3
Si queremos añadir un tipo de ficheros:
git add *.html
Para subir todos los archivos y modificaciones pendientes de pasar al stage y que estén en la carpeta actual (y todas las carpetas hijas):
git add .
Para ver información sobre las 3 zonas que usamos en Git, usamos el siguiente comando:
''git status''
Podemos ver un resumen de los ficheros que están en el stage y los que no están siendo controlados (versionados) por git. Los primeros aparecen en color verde y los segundos en color rojo. Otra forma de decirlo sería que lo que está en verde es lo que está en el stage y lo que está en rojo es lo que está en el working directory.
Poner captura con ejemplo de lo anterior
git status es un comando muy útil y es recomendable lanzar cada vez que ejecutemos algún comando de git. También ofrece “pistas”.
Si queremos quitar modificaciones del stage (por algún error):
git reset HEAD <RUTA_FICHERO>
Por ejemplo, si queremos quitar el fichero fichero1:
git reset HEAD archivo1
También se puede emplear git restore --staged <RUTA_FICHERO>
Si queremos quitar todos los cambios que haya en el stage:
git reset HEAD .
En realidad, el comando git reset sirve para otra cosa que se verá más adelante.
Para deshacer cambios en el working directory, es decir, para que Git deje los archivos modificados como estaban en el último commit:
git checkout -- <RUTA_FICHERO>
Por ejemplo, si queremos volver al estado anterior de todos los archivos del working directory:
git checkout -- .
Esta operación es irreversible, no se puede deshacer.
Finalmente, si queremos borrar un archivo tenemos dos opciones. Si el borrado lo hacemos mediante las operaciones del sistema operativo, Git lo entiende como una modificación del proyecto y podremos subirlo al stage para luego hacer un commit con ese borrado.
Si usamos el método Git:
git rm <RUTA_FICHERO>
Git lo borra del sistema de ficheros y añada esa modificación al stage, ahorrándonos un paso respecto a la versión usando el sistema operativo.
Si queremos quitar un archivo del repositorio sin borrarlo del working directory:
git rm --cached <RUTA_FICHERO>
Esto es útil cuando llegado un momento queremos empezar a ignorar un fichero que antes estábamos siguiendo (el .gitignore aquí no serviría).
Tras “desversionar” lo que queremos, ahora sí podremos modificar el .gitignore para añadir ese fichero o ficheros que no queremos que Git versione.
Realizando el commit
Un commit siempre necesita un mensaje descriptivo (es obligatorio):
git commit -m "Mensaje"
Si queremos que abra el editor por defecto para que luego añadamos el comentario:
git commit
El commit siempre se hace sobre lo que hay en el stage
Como recomendación, en los mensajes de commit habría que evitar genéricos como “Cambios”, “Nuevo archivo”, “Correcciones”, “CSS”, “Merge”… A la hora de escribir un mensaje, tenemos que explicar bien qué modificaciones sufre el proyecto y de forma muy breve. El propósito es que cuando se quiera ver el log, se pueda entender perfectamente los cambios que ha sufri oel proyecto:
git log --oneline --branches --graph
Si queremos modificar el último commit (solo el último) que hemos hecho:
git commit --amend
Esto lo haremos tras añadir cambios al stage.
Al ejecutar el comando anterior, se abrirá el mensaje del último commit (el que queremos modificar) dándonos la posibilidad de modificarlo, si queremos.
Si el commit ya se había subido al repositorio remoto, no se debe hacer --amend. Las correcciones en los commits hay que hacerlas en el repositorio local.
Otra cosa que suele pasar mucho es que hayamos hecho un commit con un mensaje incorrecto o incompleto. Si queremos corregir este mensaje, pero no tenemos más cambios, no hace falta subir nada al stage, basta con ejecutar el siguiente comando:
git commit --amend -m "Nuevo mensaje"
Si no le pasamos el mensaje por línea de comandos, git abrirá el editor por defecto.
Esta modificación cambia el hash del commit. Esto es normal porque este hash se genera a partir del contenido del título y otras cosas.
Ejemplos de commits por etapas
Añadir capturas de pantalla con las zonas de git y cómo se mueven los archivos cuando se preparan los commits.
Iniciamos un repositorio git:
git init
git status
Subiremos al stage el fichero despedidas-en.json:
git add despedidas-en.json
git status
Hacemos commit:
git commit -m "Commit inicial"
Es práctica habitual incluir el mensaje “Commit inicial” en el primer commit de un proyecto
git log --oneline --branches
git status
Subimos el fichero saludos-en.json al stage:
git add saludos-en.json
Hacemos el commit:
git commit -m "Añadidas traducciones de saludos"
git log --oneline --branches
Modificamos el fichero saludos-en.json. Ahora git reconocerá que hay un cambio en el working directory:
git status
Subimos el fichero al stage:
git add saludos-en.json
Hacemos el commit:
git commit -m "Añadida despedida"
Vemos el log:
git log --oneline --branches
Creamos un nuevo fichero en el working directory: lugares-en.json. También hacemos modificaciones en el fichero saludos-en.json. Git nos avisará de estos cambios.
git status
Añadiremos al stage solo dos ficheros:
git add lugares-en.json despedidas-en.json
Hacemos commit:
git commit -m "Añadido fichero de lugares y añadido saludo"
Vemos el log:
git log --oneline --branches
Subimos al stage saludos-en.json modificado.
git add saludos-en.json
Pero nos arrepentimos, y lo quitamos del stage:
git reset HEAD saludos-en.json
Git nos dirá que aún está pendiente el cambio de ese fichero en el working directory:
git status
Pero si queremos volver a la versión del último commit del fichero saludos-en.json:
git checkout -- .
Recordad que este último comando es irreversible
Partes de un commit
Hasta ahora hemos visto qué es un commit. Ahora veremos los datos que llevan asociados los commits:
- Autor / “committer”: nombre y correo electrónico de quien realiza el commit.
- Fecha y hora
- Mensaje
- Padre/s: un commit puede tener un padre o 2 (es raro que haya más).
- Hash de 40 caracteres: se obtieen al aplicar SHA-1 al autor, fecha, mensaje y contenido del commit.
git log
Poner ejemplo de todo lo anterior
El log
El log es el listado de commits a lo largo del tiempo.
Comandos para trabajar con el log
Ver el histórico de commits:
git log [rama]
Si no se indica la rama, git log nos mostraría el nombre de la rama en la que nos encontremos.
Para ver el log de forma compacta:
git log --oneline
Así veremos un commit por línea. No mostrará autor ni fecha.
Ver el log de todas las ramas:
git log --branches
Ahora podemos mezclar los modificadores para ver el log en formato compacto y de todas las ramas:
git log --oneline --branches
Al trabajar con ramas, es útil tener una representación gráfica de las divergencias, cómo se ha bifurcado el código:
git log --graph --branches
Podemos crear alias para comandos largos que usemos habitualmente:
git config --global alias.milog "log --oneline --branches --graph"
Habremos creado un alias llamado milog, y para usarlo:
git milog
Para ver el contenido de un commit:
git show <HASH_COMMIT>
Poner ejemplo del comando anterior
Con el comando anterior podremos ver las diferencias con el commit anterior. El formato que es usa es el de los parches (patch)
Podemos referenciar un commit de varias maneras:
- Por su identificador hash SHA-1 (lo que vimos anteriormente)
- Su posición respecto al HEAD
- Por el nombre de una rama
- Por su posición en el listado de reflog:
HEAD@{n}
git show HEAD
Commit anterior al commit apuntado por HEAD:
git show HEAD~1
Es lo mismo que
git show HEAD~
Dos commits antes:
git show HEAD~2
Es lo mismo que
git show HEAD~~
Esto es muy útil cuando queremos deshacer commits.
Si un commit tiene más de un padre, usamos la notación siguiente:
git show HEAD^1
Haremos referencia al primer padre.
git show HEAD^2
Haremos referencia al segundo padre.
Referenciando un commit por la rama que apunta:
git show nombre_rama
git reflog nos enseña una secuencia de movimientos de la referencia HEAD. Esto es útil si hemos “perdido” un commit.
git checkout HEAD@{1}
Nos habremos ido al commit al que apuntaba HEAD en cierto momento.
Poner gráfico con detached HEAD
Saltos entre commits, reset y reflog
Saltar a otro commit o rama:
git checkout <COMMIT o RAMA>
Para el nombre el commit podemos usar las referencias que vimos en el apartado anterior.
El checkout hará dos cosas: moverá el HEAD de donde esté al que estamos indicando ; la segunda cosa es que va a coger el working directory tal cual está y lo va a transformar al estado en el que estaba en el commit al que nos movemos.
Para “borrar” commits:
git reset <COMMIT>
Realmente no se borran, sencillamente desaparecen de nuestra vista. Esto permite que podamos recuperarlos en cualquier momento (si no ha pasado demasiado tiempo y ha pasado el recolector de basura de Git).
Por ejemplo, si quremos deshacer los últimos 3 commits:
git reset HEAD~3
El comando reset tiene 3 modificadores:
--soft: los cambios hechos en los commits que borramos, git los coloca en el stage. Este método se suele usar para comprimir commits.--mixed: los cambios hechos en los commits que borramos, git los coloca en el working directorio. Es la opción por defecto.--hard: los cambios hechos en los commits que borramos, desaparecen (de la vista).
Cuando desarrollamos en un equipo y queremos ver quién fue la última persona que hizo cada cambio:
git blame <RUTA>
Es útil para seguir una traza y poder encontrar quién hizo qué cosa.
Apartar cambios con stash
stash sirve para apartar cambios (en el working directory o en el stage) temporalmente.
Esto es muy útil cuando Git no nos deja cambiarnos de rama o commit (checkout) porque esos cambios machacarían nuestros ficheros.
stash es una pila, lo que entra queda en la parte superior.
git stash siempre va acompañado de un comando.
Para coger unos cambios y meterlos en la pila de “stashes”:
git stash [push -m "Mensaje"]
Listar todo lo que hay en el stash:
git stash list
Poner ejemplos del comando anterior
Si queremos ver el contenido de cierto stash:
git stash show [stash@{n}] [-p]
Para sacar los cambios del stash y colocarlos en el working directory (git stash siempre los colocará ahí):
git stash apply [stash@{n}]
El comando anterior no quita el stash de la pila. Si además queremos quitarlo de la pila, usaremos este otro comando:
git stash pop [stash@{n}]
Si queremos borrar de la pila un stash sin aplicarlo:
git stash drop [stash@{n}]
Cómo deshacer
Algo muy importante de Git es que casi todo se puede deshacer. Todo lo que vaya a un commit quedará para la historia (hasta que llegue el recolector de basura).
Repasemos varias maneras de deshacer cambios.
Cambios en el working directory
Deshacer cambios en el working directory (todos los cambios):
git checkout -- .
Recordemos que el anterior comando es de las pocas cosas en git que no se pueden deshacer.
Cambios en el stage
Deshacer lo que haya en el stage:
git reset HEAD .
Ahora sabemos que el uso principal de git reset es para volver a commits anteriores. Sin embargo, lo usamos también para quitar cambios del stage porque git reset HEAD . vuelve al commit actual (el mismo), pero como por defecto se aplica el modificador --mixed, esto hace que quite los cambios del stage y los coloque en el working directory.
Deshacer commits
git reset <COMMIT>
Deshacer un commit --amend:
git reflog
Así vemos los commits ocultos porque vemos los movimientos de HEAD. Cuando lo hayamos identificado, para recuperarlo:
git checkout <COMMIT>
Esto nos pondrá en el modo “detached HEAD”. Falta que la rama apunte también al mismo commit que HEAD. Para mover la referencia de una rama:
git branch -f master
Suponiendo que la rama master sea rama que nos interesa mover. De esta forma ya tendríamos tanto HEAD como master apuntando al commit que recuperamos. El último paso es hacer un checkout para que HEAD esté apuntando a master:
git checkout master
Podemos pensar que para recuperar un commit “oculto” lo que hacemos es meterlo en un itinerario existente, es decir, en una rama.
Deshacer un reset
Tras hacer un reset, los commits desaparecen de nuestra vista, pero podemos recuperarlos. Los podemos ver con:
git reflog
E identificamos el último commits de los que queremos recueprar.
Ahora nos llevaremos HEAD y master del momento actual a ese último commit que queremos recuperar:
git checkout <COMMIT>
Ahora llevaremos la rama:
git branch -f master
Para enlazar HEAD con master (y salir de “detached HEAD”):
git checkout master
Poner ejemplos de todo lo anterior
Resolver problemas derivados del detached HEAD
Cuando por ejemplo estamos haciendo commits en estado “detached HEAD”, no avanza la rama. Las ramas avanzan solo si HEAD la hace avanzar.
git log --oneline --graph
No ponemos --branches para poder ver HEAD.
Para solucionarlo, tenemos que mover la rama a donde esté HEAD:
git branch -f master
Nos quedaría la asociación de HEAD con master, es decir pasar de (HEAD, master) a (HEAD → master):
git checkout master
Poner ejemplos de todo lo anterior
Otra situación que se nos puede dar es que hagamos un commit en un estado anterior al commit en el que está la rama master.
Eso origina un nuevo itinerario, pero es un itinerario que está fuera de la rama master (no tiene ninguna referencia de rama asociada). Si nos cambiamos de rama, el itinerario anterior quedará oculto a la vista.
Para recuperar, nos ayudamos de:
git reflog
Identificamos el commit que nos interesa y
git checkout commit
Hemos vuelto al “itinerario sin nombre”. Para resolver esto, crearemos una referencia de rama:
git branch experimento
Ahora ya aparece el itinerario en los logs.
Seguimos en “detached HEAD”, así que para que se enlacen HEAD con experimento:
git checkout experimento
Ignorar archivos que ya están siendo versionados
Borrar del repositorio, pero no del working directory:
git rm --cached <RUTA_FICHERO>
El archivo dejará de estar versionado. Luego podremos añadirlo a un .gitignore para que de ahí en el futuro git deje de tenerlo en cuenta.
Conclusión
Hemos visto cómo se preparan los commits: en el stage. Al hacer un commit, “hacemos una foto” al stage.
Los commits no se hacen periódicamente, solo cuando es necesario que vaya a haber cambios en el log.
git reflog es muy útil para cuando queramos deshacer cambios o investigar los cambios que ha habido en nuestro repositorio.
Recursos
- Capítulo del libro Pro Git con una explicación sobre el comando git reset:
- Capítulo del libro Pro Git con una explicación sobre el Working Directory, el stage y el repositorio local:
- Capítulo del libro Pro Git con un resumen sobre cómo deshacer cosas en el Working Directory y en el stage:
