Git
Chapters ▾ 2nd Edition

7.1 Herramientas de Git - Revisión por selección

Hasta ahora, ya has aprendido la mayoría de los comandos diarios y el flujo de trabajo que necesitas para manejar y mantener un repositorio de Git para tu control del código fuente. Has conseguido cumplir con las tareas básicas de seguimiento y has agregado archivos, además has aprovechado el poder del area de staging y has conocido el tema de branching y merging.

Ahora vas a explorar unas cuantas cosas bastantes poderosas que Git puede realizar y que no necesariamente vas a usar en tu día a día, pero que puedes necesitar en algún momento.

Revisión por selección

Git te permite especificar ciertos commits o un rango de éstos de muchas maneras. No son necesariamente obvias, pero es útil conocerlas.

Revisiones individuales

Obviamente se puede referir a un “commit” por el hash SHA-1 que se le asigna, pero también existen formas más amigables de referirse a los commits. Esta sección delinea varias maneras en las que se puede referir a un “commit” indiviual.

SHA-1 corto

Git es lo suficientemente inteligente como para descifrar el “commit” al que te refieres si le entregas los primeros caracteres, siempre y cuando la parte de SHA-1 sea de al menos 4 caracteres y no sea ambigua - esto quiere decir, que solamente un objeto en el repositorio actual comience con ese SHA-1 parcial.

Por ejemplo, para ver un “commit” específico, supongamos que se utiliza el comando git log y se identifica el “commit” donde se agregó cierta funcionalidad:

$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    fixed refs handling, added gc auto, updated tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    added some blame and merge stuff

En este caso, se escoge 1c002dd..... Si utilizas git show en ese “commit”, los siguientes comandos son iguales ( asumiendo que las versiones cortas no son ambiguas):

$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b
$ git show 1c002dd4b536e7479f
$ git show 1c002d

Git puede descubrir una abreviación corta y única del valor SHA-1. Si añades --abbrev-commit al comando git log, el resultado utilizará los valores cortos pero manteniéndolos únicos; por default utiliza siete caracteres pero los hace más largos de ser necesario para mantener el SHA-1 sin ambigüedades:

$ git log --abbrev-commit --pretty=oneline
ca82a6d changed the version number
085bb3b removed unnecessary test code
a11bef0 first commit

Generalmente, de ocho a diez caracteres son más que suficientes para ser únicos en un proyecto.

Como un ejemplo, el kernel Linux, que es un proyecto bastante grande con alrededor de 450 mil “commits” y 3.6 millones de objetos, no tiene dos objetos cuyos SHA-1s se superpongan antes de los primeros 11 caracteres.

Nota
UNA BREVE NOTA RESPECTO A SHA-1

Mucha gente se preocupa de que en cierto momento, fruto del azar, tendrán dos objetos en su repositorio cuyos hash tendrán el mismo valor SHA-1. Pero, ¿entonces qué?

Si sucede que realizas un “commit” y el objeto tiene el mismo hash que un objeto previo en tu repositorio, Git verá el objeto previo en tu base de datos y asumirá que ya estaba escrito. Si intentas mirar el objeto otra vez, siempre tendrás la data del primer objeto.

Sin embargo, debes ser consciente de cuán ridículamente improbable es este escenario. El digest SHA-1 es de 20 bytes o 160 bits. El número de objetos aleatorios necesario para asegurar un 50% de probabilidades de una única colisión bordea el 280 (la fórmula para determinar la propabilidad de colisión es p = (n(n-1)/2) * (1/2^160)). 280 es 1.2 x 1024 o 1 millón de millones de millones. Esto es 1,200 veces el ńumero de granos de arena en la Tierra.

Aquí hay un ejemplo para darte una idea de lo que tomaría para conseguir una colisión de SHA-1. Si todos los 6.5 mil millones de humanos en la Tierra estuvieran programando, y cada segundo, cada uno produjera el código equivalente a toda la historia del kernel Linux (3.6 mil millones de objetos en Git) e hicieran push en un enorme repositorio Git, tomaría aproximadamente 2 años hasta que el repositorio tuviera suficientes objetos para un 50% de probabilidades de que ocurriera una única colisión en los SHA-1 de los objetos. Existe una probabilidad más alta de que cada miembro de tu equipo de programación sea atacado y asesinado por lobos en incidentes sin relación y todo esto en la misma noche.

Referencias por rama

El camino más sencillo para especificar un “commit” requiere que este tenga una rama de referencia apuntando al mismo. Entonces, se puede usar el nombre de la rama en cualquier comando Git que espere un objeto “commit” o un valor SHA-1. Por ejemplo, si se quiere mostrar el último objeto “commit” de una rama, los siguientes comandos son equivalentes, asumiendo que la rama topic1 apunta a ca82a6d:

$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1

Si se quiere ver a qué SHA-1 apunta un rama en específico, o si se quiere ver lo que cualquiera de estos ejemplos expresa en terminos de SHA-1s, puede utilizar una herramienta de plomería de Git llamada rev-parse. Se puede ver [ch10-git-internals] para más información sobre las herramientas de plomería; básicamente, rev-parse existe para operaciones de bajo nivel y no está diseñado para ser utilizado en operaciones diarias. Aquí puedes correr rev-parse en tu rama.

$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949

Nombres cortos de RefLog

Una de las cosas que Git hace en segundo plano, mientras tu estás trabajando a distancia, es mantener un “reflog” - un log de a dónde se apuntan las referencias de tu HEAD y tu rama en los últimos meses.

Se puede ver el reflog utilizando git reflog:

$ git reflog
734713b HEAD@{0}: commit: fixed refs handling, added gc auto, updated
d921970 HEAD@{1}: merge phedders/rdocs: Merge made by recursive.
1c002dd HEAD@{2}: commit: added some blame and merge stuff
1c36188 HEAD@{3}: rebase -i (squash): updating HEAD
95df984 HEAD@{4}: commit: # This is a combination of two commits.
1c36188 HEAD@{5}: rebase -i (squash): updating HEAD
7e05da5 HEAD@{6}: rebase -i (pick): updating HEAD

Cada vez que la punta de tu rama es actualizada por cualquier razón, Git guarda esa información en este historial temporal. Y es así como se puede especificar commits antiguos con esta información. Si se quiere ver el quinto valor anterior a tu HEAD en el repositorio, se puede usar la referencia @{n} que se ve en la salida de reflog:

$ git show HEAD@{5}

También se puede utilizar esta sintaxis para ver dónde se encontraba una rama dada una cierta cantidad de tiempo. Por ejemplo, para ver dónde se encontraba tu rama master ayer, se puede utilizar

$ git show master@{yesterday}

Esto muestra a dónde apuntaba tu rama el día de ayer. Esta técnica solo funciona para información que permanece en tu reflog, por lo que no se puede utilizar para ver commits que son anteriores a los que aparecen en él.

Para ver información sobre reflog en el formato de git log, se puede utilizar git log -g:

$ git log -g master
commit 734713bc047d87bf7eac9674765ae793478c50d3
Reflog: master@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: commit: fixed refs handling, added gc auto, updated
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    fixed refs handling, added gc auto, updated tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Reflog: master@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: merge phedders/rdocs: Merge made by recursive.
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

Es importante notar que la información de reflog es estríctamente local - es un log de lo que se ha hecho en el repositorio local. Las referencias no serán las mismas en otra copia del repositorio; y justo después de que se ha inicializado el repositorio, se tendrá un reflog vacío, dado que no ha ocurrido ninguna actividad todavía en el mismo. Utilizar git show HEAD@{2.months.ago} funcionará solo si se clonó el proyecto hace al menos dos meses - si se clonó hace cinco minutos, no se obtendrán resultados.

Referencias por ancestros

Otra forma principal de especificar un “commit” es por sus ancestros. Si se coloca un ^ al final de la referencia, Git lo resuelve como el padre de ese “commit”. Supongamos que se mira a la historia de un proyecto:

$ git log --pretty=format:'%h %s' --graph
* 734713b fixed refs handling, added gc auto, updated tests
*   d921970 Merge commit 'phedders/rdocs'
|\
| * 35cfb2b Some rdoc changes
* | 1c002dd added some blame and merge stuff
|/
* 1c36188 ignore *.gem
* 9b29157 add open3_detach to gemspec file list

Entoces, se puede ver los commits previos especificando HEAD^, lo que significa “el padre de HEAD”:

$ git show HEAD^
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

También se puede especificar un número despúes de ^ - por ejemplo, d921970^2 significa “el segundo padre de d921970.” Esta sintaxis es útil solamente para fusiones confirmadas, las cuales tienen más de un padre. El primer padre es la rama en el que se estaba al momento de fusionar, y el segundo es el “commit” en la rama en la que se fusionó:

$ git show d921970^
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    added some blame and merge stuff

$ git show d921970^2
commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548
Author: Paul Hedderly <paul+git@mjr.org>
Date:   Wed Dec 10 22:22:03 2008 +0000

    Some rdoc changes

La otra manera principal de especificar ancestros es el ~. Este también refiere al primer padre, asi que HEAD~ y HEAD^ son equivalentes. La diferencia se vuelve aparente cuando se especifica un número. HEAD~2 significa “el primer padre del primer padre,” o “el abuelo” - este recorre el primer padre las veces que se especifiquen. Por ejemplo, en el historial listado antes, HEAD~3 sería

$ git show HEAD~3
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    ignore *.gem

Esto también puede ser escrito HEAD^^^, lo que también es, el primer padre del primer padre del primer padre:

$ git show HEAD^^^
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    ignore *.gem

También se puede combinar estas sintaxis - se puede obtener el segundo padre de la referencia previa ( asumiendo que fue una fusión confirmada) utilizando HEAD~3^2, y así sucesivamente.

Rangos de Commits

Ahora que ya puede especificar commits individuales, vamos a a ver cómo especificar un rango de commits. Esto es particularmete útil para administrar las ramas - si se tienen muchas ramas, se puede usar un rango de especificaciones para contestar preguntas como, “¿Qué trabajo está en esta rama y cuál no hemos fusionado en la rama principal?”

Dos puntos

La forma más común de especificar un rango es mediante la sintaxis de doble punto. Esto básicamente pide a Git que resuelva un rango de commits que es alcanzable desde un “commit” pero que no es alcanzable desde otro. Por ejemplo, digamos que se tiene un historial de commits que se ve como Example history for range selection..

Ejemplo historial para un rango de seleción.
Figura 137. Example history for range selection.

Se quiere ver qué se encuentra en la rama experiment que no ha sido fusionado a la rama master todavía. Se puede pedir a Git que muestre el log de solamente aquellos commits con master..experiment - eso significa “todos los commits alcanzables por experiment que no son alcanzables por master.” Para ser breves y claros en este ejemplo. Se usarán las letras de los objetos “commit” del diagrama en lugar del log para que se muestre de la siguiente manera:

$ git log master..experiment
D
C

Si, por otro lado, se quiere lo opuesto - todos los commits en master que no están en experiment - se pueden invertir los nombres de las ramas. experiment..master``muestra todo lo que hay en `master que no es alcanzable para experiment:

$ git log experiment..master
F
E

Esto es útil si se quiere mantener la rama experiment actualizada y previsualizar lo que se está a punto de fusionar. Otro uso bastante frecuente de esta sintaxis es para ver lo que se está a punto de publicar en remote:

$ git log origin/master..HEAD

Este comando muestra cualquier “commit” en tu rama actual que no está en la rama master del remoto origin. Si se corre un git push y la rama de seguimiento actual es origin/master, los commits listados por git log origin/master..HEAD son los commits que serán transferidos al servidor. También se puede dejar de lado la sintaxis para que Git asuma la HEAD. Por ejemplo, se puede obtener el mismo resultado que en el ejemplo previo tipiando git log origin/master.. - Git sustituye HEAD si un lado está faltando.

Múltiples puntos

La sintaxis de dos puntos es útil como una abreviatura; pero tal vez se desea especificar más de dos ramas para indicar la revisión, como puede ser revisar qué commits existen en muchas ramas que no se encuentran en la rama en la que se realiza el trabajo actualmente. Git permite realizar esto utilizando el caracter ^ o el comando --not antes de cualquier referencia de la cual no deseas ver los commits alcanzables.

$ git log refA..refB
$ git log ^refA refB
$ git log refB --not refA

Esto es bueno porque con esta sintaxis se puede especificar más de dos referencias en una consulta, lo que no se puede hacer con la sintaxis de dos puntos. Por ejemplo, si se quiere ver todos los commits que son alcanzables desde refA o refB pero no desde refC, se puede escribir lo siguiente:

$ git log refA refB ^refC
$ git log refA refB --not refC

Esto lo convierte en un sistema de consultas muy poderoso que debería ayudar a descubrir qué hay en tus ramas.

Tres puntos

La última sintaxis de selección de rangos es la de tres puntos, que especifica todos los commits que son alcanzables por alguna de dos referencias, pero no por las dos al mismo tiempo. Mira atrás al ejemplo de historial de commits en Example history for range selection.. Si se quiere ver lo que está en master o experiment pero no en ambos, se puede utilizar

$ git log master...experiment
F
E
D
C

Nuevamente, esto entrega la salida normal del log pero muestra solo la información de esos cuatro commits, apareciendo en el tradicional ordenamiento por fecha de “commit”.

Un cambio común para utilizar con el comando log, en este caso, es --left-right, el cual muestra en qué lado del rango se encuentra cada “commit”. Esto ayuda a hacer la información más útil:

$ git log --left-right master...experiment
< F
< E
> D
> C

Con estas herramientas, se puede hacer saber más facilmente a Git qué “commit” o commits desea inspeccionar.

scroll-to-top