Archivo de la categoría: comandos

Cómo deshacer el último commit en git

Aquí os dejo una manera sencilla de dar marcha atrás si habéis hecho un commit y os habéis arrepentido de hacerlo. El motivo por el que queréis “borrarlo” puede ser múltiple: porque el trabajo no está terminado y queréis continuar trabajando, habéis introducido un bug sin daos cuenta o sencillamente os habéis equivocado y lo habéis hecho antes de tiempo.

Existen dos maneras de borrar ese commit:

  • Eliminando junto al commit las modificaciones que este contiene
  • Recuperándolas en el área de trabajo para seguir trabajando en ellas

En ambos casos, el comando que utilizaremos será “git reset”.

Deshacer el commit perdiendo las modificaciones

Supongamos que tenemos nuestro repositorio en el siguiente estado:

Voila_Capture283y queremos deshacer el último commit. En este primer caso, queremos desechar los cambios introducidos en ese commit que contiene una serie de tests funcionales.

Para ello, ejecutamos el comando:

git reset --hard HEAD~1

Tras ejecutar el comando, el estado del repositorio es el siguiente:

Repositorio tras borrar el commit

Como podéis ver, el commit 600cc08 ha desaparecido que era lo que queríamos. Además, la rama activa se ha desplazado un commit hacia abajo y nuestro área de trabajo ha quedado en el estado del commit 6eb9f2d. Los tests funcionales que estaban en el commit se han perdido y tendríamos que recurrir al reflog para recuperarlos.

La sintaxis HEAD~1 del comando anterior la podríamos traducir como “El commit al que está apuntando la rama activa menos uno”. Si hubiésemos ejecutado el comando:

git reset --hard HEAD~3

en lugar de acabar en el commit 6eb9f2d (uno por detrás) habríamos acabado en 63db9fa (tres por detrás).

Deshacer el commit manteniendo las modificaciones

Existe la posibilidad de eliminar el commit pero manteniendo las modificaciones que contiene ese commit en el área de trabajo. ¿Y por qué querríamos hacer esto? Por varios motivos, por ejemplo por que los tests funcionales del commit 600cc08 están incompletos, son incorrectos o he introducido algún bug en él.

Partiendo de nuevo del mismos estado inicial de antes:

Estado inicial del repositorio.ejecutaríamos el siguiente comando:

git reset HEAD~1

Tras lo cual, el estado del repositorio sería:

Voila_Capture285Si miráis el estado veréis los siguiente:

  • Al igual que en el caso anterior, el commit 600cc08 ha desaparecido
  • La rama activa (rest) ha pasado al apuntar al commit 6eb9f2d
  • A diferencia del  caso anterior, el área de trabajo contiene las modificaciones que estaban en el commit que acabamos de borrar.

Así que podemos seguir trabajando, corregir el bug o completar los tests que habíamos dejado incompletos y hacer un nuevo commit con los cambios completos. ¡Así de fácil!

Muy útil pero…

Muy importante tener en cuenta que estas dos operaciones sobreescriben la historia del repositorio ¡estamos borrando un commit!. Si estamos trabajando en local y no hemos hecho push a nuestro remoto no hay ningún problema. Si ha habéis hecho push de este commit tened en cuenta que vuestros compañeros lo seguirán viendo si alguna de sus ramas lo referencia.

Espero que os haya resultado útil.

 

 

Forzando los merge commits

Una de las preguntas más habituales que se hacen en los cursos de git que imparto regularmente, y que también se me ha planteado en algunas de las reuniones de desarrolladores en las que participo es la siguiente: ¿Cómo evito un fast-forward para dejar constancia en el repositorio de dónde empieza y termina una rama?

Forzar merge commits

Lo más claro, como siempre, es verlo con un ejemplo. Imaginad que llegáis por la mañana al trabajo y empezáis como siempre a picar código como locos en la rama devel. De repente surge la necesidad de atender un bug. Siguiendo las buenas prácticas, lo que haremos será crear una rama HotFix, arreglar el problema haciendo cuantos commits necesitemos a esa rama y finalmente hacer un merge a la rama que corresponda cuando terminemos de corregir el bug.

En la siguiente captura se muestra el estado del repositorio justo después de haber hecho el último commit a la rama hotfix_14523 que arregla el problema en cuestión:

Estado del repositorio al terminar el hotfix 15423

Si en este momento se hace un merge de la rama hotfix_15423 a la rama devel, se hará un fast-forward:

$ git checkout devel
$ git merge hotfix_15423
Updating 2d1d1c4..2b9d126
Fast-forward
Gemfile | 2 ++
app/views/home/index.html.erb | 1 +
2 files changed, 3 insertions(+)

Así queda el grafo después de hacer el merge:Estado del repositorio tras hacer un merge con fast-forward de la rama hotfix_15423

Como vemos, mirando la historia del repositorio, no queda constancia de cuándo se empezó el desarrollo de la rama hotfix_15423 y cuándo se incorporó a la rama devel. Una solución podría ser etiquetar los commits correspondientes, como se muestra en la siguiente captura:Usando etiquetas para indicar el inicio y final de una rama

Esta solución es válida cuando tenemos pocas ramas. Si tenemos muchas ramas de las que queramos saber su inicio y final, podemos acabar con un número de etiquetas difícil de gestionar.

Forzar un merge commit

Volvamos a la situación inicial:

$ git reset --hard 2d1d1c4
HEAD is now at 2d1d1c4 Update README.rdoc

Estado del repositorio al terminar el hotfix 15423

En esta situación, volvemos a ejecutar el merge de la siguiente forma:

$ git merge --no-ff hotfix_15423 -m'Incorporando rama hotfix_15423'
Merge made by the 'recursive' strategy.
 Gemfile | 2 ++
 app/views/home/index.html.erb | 1 +
 2 files changed, 3 insertions(+)

Después de ejecutar el comando, este es el estado del repositorio:

Incoporando hotfix15423 en devel con la opción --no-ff

Haciéndolo de esta forma, en lugar de hacerse un fast-forward, vemos que se crea un merge-commit, lo que resulta en una bifurcación que indica claramente el fin y el inicio de la rama hotfix_15423.

Si sois de los que os gusta ver claramente dónde empiezan y terminan vuestras ramas, este pequeño truco es resultará de utilidad.

Opciones del comando git add

Un comando con el que todos estamos familiarizados es git add. Cuando nos iniciamos en el uso de git, este es de los que no faltan en las recetas que podemos en ver en cualquier tutorial. Es de los primeros que empezamos a utilizar cuando aprendemos git ¡En el libro de git de Scott Chacon aparece en la sección 2.2!

Es una pieza fundamental del flujo básico de git ya que es el comando que mueve al índice las modificaciones que hayamos realizado. El índice es un snapshot del contenido del área de trabajo en un momento dado. Este snapshot es el que posteriormente se convertirá en un commit.

La receta de git add

La forma más habitual de invocar el comando git-add es:

$ git add .

Este comando añade al índice cualquier fichero nuevo o que haya sido modificado:

$ git add .
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#       new file: app/assets/javascripts/home.js.coffee
#       new file: app/assets/stylesheets/home.css.scss
#       new file: app/controllers/home_controller.rb
#       new file: app/helpers/home_helper.rb
#       new file: app/views/home/index.html.erb
#       modified: config/database.yml
#       modified: config/routes.rb
#       new file: test/functional/home_controller_test.rb
#       new file: test/unit/helpers/home_helper_test.rb
#
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#       deleted: app/assets/images/rails.png

Sin embargo, los ficheros borrados no se actualizan en el índice. En este caso, tendríamos que ejecutar el comando git rm app/assets/images/rails.png:

$ git rm app/assets/images/rails.png
rm 'app/assets/images/rails.png'
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# deleted: app/assets/images/rails.png
# new file: app/assets/javascripts/home.js.coffee
# new file: app/assets/stylesheets/home.css.scss
# new file: app/controllers/home_controller.rb
# new file: app/helpers/home_helper.rb
# new file: app/views/home/index.html.erb
# modified: config/database.yml
# modified: config/routes.rb
# new file: test/functional/home_controller_test.rb
# new file: test/unit/helpers/home_helper_test.rb
#

Las opciones -u y -A del comando git-add añaden al índice los ficheros eliminados, aunque gestionan de manera diferente los ficheros nuevos. Veamoslo usando este mismo ejemplo.

git-add -u

Partimos de la siguiente área de trabajo:

$ git status
# On branch master
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#    deleted: app/assets/images/rails.png
#    modified: config/routes.rb
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/assets/javascripts/home.js.coffee
#    app/assets/stylesheets/home.css.scss
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/
no changes added to commit (use "git add" and/or "git commit -a")

La opción -u sólo añade al índice aquellos ficheros que ya estén siendo monitorizados por git, así que en este caso, únicamente se subirán al índice rails.png y routes.rb:

$ git add -u
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#    deleted: app/assets/images/rails.png
#    modified: config/routes.rb
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/assets/javascripts/home.js.coffee
#    app/assets/stylesheets/home.css.scss
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/

La opción -u acepta un patrón de búsqueda. Si este patrón está vacío, el resultado es que se actualicen todos los ficheros borrados o modificados en el área de trabajo. Si el patrón no está vacío, sólo se actualizarán en el índice los ficheros que encajen con el patrón. En el siguiente ejemplo, ejecutamos el mismo comando para añadir únicamente los ficheros de la carpeta config:

$ git add -u config/*
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#    modified: config/routes.rb
#
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#    deleted: app/assets/images/rails.png
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/assets/javascripts/home.js.coffee
#    app/assets/stylesheets/home.css.scss
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/

git-add -a

Esta opción funciona como la opción -u añadiendo también a la búsqueda los ficheros del área de trabajo. El resultado es que los ficheros que no estén siendo monitorizados también se añadirán al índice.

Partimos de la siguiente situación:

$ git status
# On branch master
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#    deleted: app/assets/images/rails.png
#    modified: config/routes.rb
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/assets/javascripts/home.js.coffee
#    app/assets/stylesheets/home.css.scss
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/
no changes added to commit (use "git add" and/or "git commit -a")

Al ejecutar el comando git add -A, se añadirán al índice los ficheros borrados, los modificados y los nuevos:

$ git add -A
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#    deleted: app/assets/images/rails.png
#    new file: app/assets/javascripts/home.js.coffee
#    new file: app/assets/stylesheets/home.css.scss
#    new file: app/controllers/home_controller.rb
#    new file: app/helpers/home_helper.rb
#    new file: app/views/home/index.html.erb
#    modified: config/routes.rb
#    new file: test/functional/home_controller_test.rb
#    new file: test/unit/helpers/home_helper_test.rb

Al igual que la opción -u, esta opción también acepta un patrón. En el siguiente ejemplo, actualizamos el índice con las modificaciones, ficheros nuevos y ficheros borrados sólo de la carpeta app/assets:

$ git add -A app/assets
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
#    deleted: app/assets/images/rails.png
#    new file: app/assets/javascripts/home.js.coffee
#    new file: app/assets/stylesheets/home.css.scss
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
#    modified: config/routes.rb
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
#    app/controllers/home_controller.rb
#    app/helpers/home_helper.rb
#    app/views/home/
#    test/functional/home_controller_test.rb
#    test/unit/helpers/

git-add -n

Esta opción es muy práctica ya que nos mostrará en pantalla lo que el comando git-add haría sin actualizar el índice. Continuando con el comando anterior (git add -A app/assets) si quisiésemos ver qué ficheros se actualizarán en el índice sin realmente actualizarlos, haríamos los siguiente:

$ git add -n -A app/assets
remove 'app/assets/images/rails.png'
add 'app/assets/javascripts/home.js.coffee'
add 'app/assets/stylesheets/home.css.scss'

Resumen

Hemos visto tres opciones del comando git-add: -u, -A y -n. Las dos primeras nos permiten actualizar en el índice directamente ficheros borrados sin necesidad de ejecutar el comando git-rm. La última opción, -n, simula lo que hará el comando git-add sin realmente actualizar el índice. Los tres son opciones útiles que de haberlas conocido cuando empecé con git hace años me habrían ahorrado unos cuantos comandos.

Referencias