Capítulo 4. Compromete
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Una confirmación es una instantánea que captura el estado actual de un repositorio en un momento determinado. Las instantáneas de confirmación se encadenan, y cada nueva instantánea apunta a su predecesora. Con el tiempo, una secuencia de cambios se representa como una serie de confirmaciones.
Git utiliza una confirmación para registrar los cambios en un repositorio. A primera vista, un commit de Git es comparable a un check-in o commit de otros sistemas de control de versiones. Sin embargo, las similitudes son sólo superficiales. Bajo el capó, la mecánica interna de cómo Git crea y gestiona las confirmaciones es totalmente única.
Cuando realizas una confirmación, Git toma una instantánea del estado actual del directorio índice y la almacena en el almacén de objetos, tal y como se explicó brevemente en el Capítulo 2. La instantánea no contiene una copia de cada archivo y directorio del índice. En su lugar, Git compara el estado actual del índice con la instantánea de la confirmación anterior y obtiene una lista de los archivos y directorios afectados cuando estás creando una nueva confirmación. Basándose en esta lista, Git crea nuevos objetos blob para cualquier archivo que haya cambiado y nuevos objetos árbol para cualquier directorio que haya cambiado, y reutiliza cualquier objeto blob o árbol que no haya cambiado.
En el capítulo 5 veremos cómo preparar el directorio índice para una confirmación. En este capítulo, nos centraremos en lo que ocurre cuando realizas una confirmación. Primero exploraremos cómo se introducen las confirmaciones y comprenderemos la importancia de los conjuntos de cambios atómicos. Después veremos cómo identificar las confirmaciones y cómo ver los historiales de las mismas.
Git se adapta bien a los commits frecuentes y proporciona un rico conjunto de comandos para manipularlos. Te mostraremos cómo varios commits, cada uno con cambios pequeños y bien definidos, también pueden conducir a una mejor organización de los cambios y a una manipulación más fácil de los conjuntos de parches.
Compromisos: Unidades de cambio registradas
Un commit es el único método para introducir cambios en un repositorio. Este mandato proporciona auditabilidad y responsabilidad. Bajo ninguna circunstancia deben cambiar los datos del repositorio sin que quede constancia del cambio mediante una confirmación. Imagina el caos que se produciría si el contenido del repositorio cambiara de algún modo y no quedara constancia de cómo ocurrió, quién lo hizo o por qué.
Los commits son introducidos explícitamente por los desarrolladores; éste es el escenario más típico. Sin embargo, hay ocasiones en las que el propio Git puede introducir una confirmación. Por ejemplo, una operación de fusión crea una nueva confirmación en el repositorio, además de cualquier confirmación realizada por los desarrolladores antes de la fusión. Aprenderás más sobre esto en el Capítulo 6.
La frecuencia con la que creas commits depende bastante de ti. Lógicamente, deberías introducir un commit en momentos bien definidos en los que tu desarrollo se encuentre en una fase latente, como cuando todas las suites de prueba pasan.
Podrías pensar que llevaría mucho tiempo comparar todo el índice con algún estado anterior, sin embargo, todo el proceso es notablemente rápido. Esto se debe a que, como recordarás del Capítulo 2, cada objeto Git tiene un hash SHA1, y si dos objetos, incluso dos subárboles, tienen el mismo hash SHA1, los objetos en comparación son idénticos. Así, Git puede evitar franjas de comparaciones recursivas podando los subárboles que tienen el mismo contenido.
Sin embargo, esto no significa que debas dudar en introducir commits con regularidad.
Conjuntos de cambios atómicos
Cada confirmación de Git representa un único conjunto de cambios atómicos respecto al estado anterior. Independientemente del número de directorios, archivos, líneas o bytes que cambien con una confirmación,1 o se aplican todos los cambios o no se aplica ninguno.
Desde la perspectiva del modelo de objetos subyacente de Git, la razón de ser de la atomicidad resulta más clara. Una instantánea de confirmación representa el estado del conjunto total de archivos y directorios modificados, lo que también significa que representa un estado de árbol determinado. Así, un conjunto de cambios entre dos instantáneas representa una transformación completa de un estado del árbol a otro. De nuevo, sólo puedes pasar de un estado a otro; no puedes hacer cambios incrementales. Discutiremos cómo derivar las diferencias entre commits en el Capítulo 7.
Como desarrollador, éste es un principio importante que no debes socavar. Considera el siguiente flujo de trabajo de mover una función de un archivo a otro. Si eliminas la función del primer archivo con un commit y luego la añades al segundo archivo con otro commit, queda un pequeño "vacío semántico" en el historial de tu repositorio durante el cual la función desaparece. Dos confirmaciones en orden inverso también son problemáticas. En cada caso, antes de la primera confirmación y después de la segunda, tu código es semánticamente coherente, pero después de la primera confirmación, el código es defectuoso.
Sin embargo, con un commit atómico que elimina simultáneamente la función del primer archivo y la añade al segundo, no aparece ese vacío semántico en el historial. Comprender este concepto te permitirá estructurar tus confirmaciones de forma más adecuada.
A Git no le importa por qué cambian los archivos. Es decir, el contenido de los cambios no importa. Puedes mover una función de un archivo a otro y esperar que se gestione como un movimiento unitario. Alternativamente, puedes confirmar la eliminación y más tarde confirmar la adición. A Git no le importa. No tiene nada que ver con la semántica de lo que hay en los archivos.
Identificar los compromisos
Cada confirmación en Git puede ser referenciada explícita o implícitamente. Ser capaz de identificar confirmaciones individuales es una tarea esencial para tus requisitos rutinarios de desarrollo. Por ejemplo, para crear una rama, debes elegir una confirmación de la que divergir; para comparar variaciones de código, debes especificar dos confirmaciones; y para editar el historial de confirmaciones, debes proporcionar una colección de confirmaciones.
Cuando haces referencia a un commit explícitamente, lo haces utilizando sus nombres de commit absolutos, y cuando haces referencia a un commit implícitamente, lo haces utilizando sus refs, symrefs o nombres de commit relativos.
Ya has visto ejemplos de referencias de confirmación explícitas y referencias de confirmación implícitas en fragmentos de código de los Capítulos 1 a 3. El identificador único de confirmación SHA1 hexadecimal de 40 dígitos es una referencia explícita, mientras que HEAD
, que siempre apunta a la confirmación más reciente de una rama, es una referencia implícita; consulta la Tabla 4-1.
Explícito | Implícito | |
---|---|---|
Identificado a través de |
Nombre absoluto de la confirmación |
Refs, symrefs, nombres de commit relativos |
Ejemplo |
|
|
A veces, cuando se habla de una confirmación concreta con un colega que trabaja con los mismos datos pero en un entorno distribuido, es mejor utilizar un nombre de confirmación que garantice que es el mismo en ambos repositorios. Por otro lado, si estás trabajando en tu propio repositorio y necesitas referirte al estado de unas confirmaciones anteriores en una rama, un simple nombre relativo funciona perfectamente.
Afortunadamente, Git proporciona muchos mecanismos diferentes para nombrar una confirmación, cada uno con ventajas y algunos más útiles que otros, dependiendo del contexto.
Nombres de compromiso absolutos
El nombre más riguroso de una confirmación es su ID de objeto, el identificador hash SHA1. El identificador hash SHA1 es un nombre absoluto, lo que significa que sólo puede referirse exactamente a una confirmación. No importa dónde se encuentre la confirmación en el historial del repositorio; el identificador hash SHA1 siempre apunta e identifica a la misma confirmación.
Cada ID de confirmación es globalmente único, no sólo para un repositorio, sino para todos y cada uno de los repositorios. Si comparas una referencia a un ID de confirmación específico en tu repositorio con el repositorio de otro desarrollador y se encuentra el mismo ID de confirmación, puedes estar seguro de que ambos tenéis la misma confirmación y el mismo contenido.
Además, como los datos que contribuyen a un ID de confirmación contienen el estado de todo el árbol del repositorio, así como el estado de la confirmación anterior, también puedes estar seguro de que ambos estáis haciendo referencia a la misma línea completa de desarrollo que conduce a la confirmación y la incluye.
Dado que un número SHA1 hexadecimal de 40 dígitos da lugar a una entrada tediosa y propensa a errores, Git te permite acortar este número a un prefijo único dentro de la base de datos de objetos de un repositorio. Veamos un ejemplo del propio repositorio de Git:
$git log -1 --pretty=oneline HEAD
30cc8d0f147546d4dd77bf497f4dec51e7265bd8 ... A regression fix for 2.37 $git log -1 --pretty=oneline 30c
fatal: ambiguous argument '30c': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this: 'git <command> [<revision>...] -- [<file>...]' $git log -1 --pretty=oneline 30cc8d
a5828ae6b52137b913b978e16cd2334482eb4c1f ... A regression fix for 2.37
Nota
Aunque un nombre de etiqueta no es un nombre globalmente único, es absoluto en el sentido de que apunta a una confirmación única y no cambia con el tiempo (a menos que lo cambies explícitamente, por supuesto).
Refs y Symrefs
Una ref apunta a un ID hash SHA1 dentro del almacén de objetos Git. Técnicamente, una ref simple puede apuntar a cualquier objeto Git, pero generalmente se refiere a un objeto de confirmación. Una referencia simbólica, o symref, es un nombre que apunta indirectamente a un objeto Git. Piensa en ello como un atajo que apunta al objeto Git real. Sigue siendo sólo una ref.
Cada ref simbólica tiene un nombre definitivo y completo que empieza por refs/
, y cada una se almacena jerárquicamente dentro del repositorio en el directorio .git/refs/. Hay básicamente tres espacios de nombres diferentes representados en refs/
:
-
refs/heads/ref
para tus sucursales locales -
refs/remotes/ref
para tus sucursales de seguimiento a distancia -
refs/tags/ref
para tus etiquetas
Los nombres de las ramas locales, los nombres de las ramas de seguimiento remoto y los nombres de las etiquetas son algunos ejemplos de referencias. Por ejemplo, una rama de características locales llamada dev
es en realidad una forma abreviada de refs/heads/dev
. Mientras que las ramas de seguimiento remoto están en el espacio de nombres refs/remotes/
, por lo que origin/main
es una forma abreviada de refs/remotes/origin/main
. Por último, una etiqueta como v1.8.17
es la abreviatura de refs/tags/v1.8.17
.
Cuando busques o hagas referencia a una referencia, puedes utilizar el nombre completo de la referencia (refs/heads/main
) o su abreviatura (main
). En caso de que busques una rama y exista una etiqueta con el mismo nombre, Git aplica una heurística de desambiguación y utiliza la primera coincidencia según esta lista de la página man git rev-parse
:
.git/ref
.git/refs/ref
.git/refs/tags/ref
.git/refs/heads/ref
.git/refs/remotes/ref
.git/refs/remotes/ref
/HEAD
La primera regla de concordancia (.git/
ref
) es la que suele utilizar Git internamente. Son , , , , y . HEAD
ORIG_HEAD
FETCH_HEAD
CHERRY_PICK_HEAD
MERGE_HEAD
Nota
Técnicamente, el nombre del directorio Git, .git, puede cambiarse. Así, la documentación interna de Git utiliza la variable $GIT_DIR
en lugar del literal .git
.
Git mantiene internamente las siguientes symrefs de forma automática por razones particulares:
HEAD
-
HEAD
siempre se refiere a la confirmación más reciente de la rama actual. Cuando cambias de rama, Git actualiza automáticamenteHEAD
para hacer referencia a la última confirmación de la nueva rama. ORIG_HEAD
-
Algunas operaciones, como fusionar y restablecer, registran la versión anterior de
HEAD
enORIG_HEAD
justo antes de ajustarla a un nuevo valor. Puedes utilizarORIG_HEAD
para recuperar o volver al estado anterior o para hacer una comparación. FETCH_HEAD
-
Cuando se utilizan repositorios remotos,
git fetch
registra las cabeceras de todas las ramas obtenidas en el archivo .git/FETCH_HEAD.FETCH_HEAD
es una abreviatura de la cabecera de la última rama obtenida y sólo es válida inmediatamente después de una operación de obtención. Utilizando esta referencia simbólica, puedes encontrar elHEAD
de las confirmaciones degit fetch
incluso si se utiliza una obtención anónima que no nombra específicamente una rama. La operaciónfetch
se trata en el Capítulo 11. MERGE_HEAD
-
Cuando una fusión está en curso, la punta de la otra rama se registra temporalmente en el symref
MERGE_HEAD
. En otras palabras,MERGE_HEAD
es el commit que se está fusionando enHEAD
. CHERRY_PICK_HEAD
-
Cuando se utiliza el cherry-picking mediante el comando
git cherry-pick
, el symrefCHERRY_PICK_HEAD
registrará los commits que hayas seleccionado para la operación prevista. El comandogit cherry-pick
se trata en el Capítulo 8.
Todas estas referencias simbólicas se gestionan mediante el comando de fontanería de bajo nivel git symbolic-ref
.
Advertencia
Aunque es posible crear tu propia rama con uno de estos nombres simbólicos especiales, no es una buena idea. Además, las versiones más recientes de Git disponen de salvaguardas que te impiden utilizar determinados nombres simbólicos (por ejemplo, HEAD
).
Existe toda una serie de variantes de caracteres especiales para los nombres de referencia. Las dos más comunes, el signo de intercalación (^
) y la tilde (~
), se describen en la siguiente sección. En otra vuelta de tuerca a las refs, se pueden utilizar dos puntos para referirse a versiones alternativas de un archivo común implicado en un conflicto de fusión. Este procedimiento se describe en el Capítulo 6.
Nombres de compromiso relativos
Además de que Git utiliza nombres de confirmación absolutos, y refs, y symrefs, Git también proporciona mecanismos para identificar una confirmación relativa a otra referencia, comúnmente la punta de una rama. Esto resulta útil cuando trabajas en tu repositorio local y necesitas hacer referencia rápidamente a cambios en confirmaciones anteriores.
De nuevo, ya has visto algunos de estos nombres, como main
y main^`
, donde main^
siempre se refiere al penúltimo commit de la rama main
. También hay otros: puedes utilizar main^^
, main~2
, e incluso un nombre complejo como main~10^2~2^2
.
Excepto el primer commit o commit raíz,2 cada confirmación se deriva de al menos una confirmación anterior y posiblemente de muchas, donde las ancestras directas se denominan confirmaciones padre. Para que una confirmación tenga varias confirmaciones padre, debe ser el resultado de una operación de fusión. Como resultado, habrá una confirmación padre por cada rama que contribuya a una confirmación de fusión.
Dentro de una misma generación, el signo de intercalación se utiliza para seleccionar un padre diferente. Dado un commit C
, C^1
es el primer padre, C^2
es el segundo padre, y C^n
es el padre n^th^
, como se muestra en la Figura 4-1.
La tilde se utiliza para retroceder antes de un padre ancestral y seleccionar una generación precedente. De nuevo, dado el commit C
, C~1
es el primer padre, C~2
es el primer abuelo y C~3
es el primer bisabuelo. Cuando hay varios padres en unageneración, se sigue al primer padre del primer padre. También puedes observar que tanto C^1
como C~1
se refieren al primer padre; cualquiera de los dos nombres es correcto, como se muestra en la Figura 4-2.
Git también admite otras abreviaturas y combinaciones. Las formas abreviadas C^
y C~
son lo mismo que C^1
y C~1
, respectivamente. Además, C^^;
es lo mismo que C^1^1
, y, como significa "el primer padre del primer padre de la confirmación C
," se refiere a la misma confirmación que C~2
.
Ten en cuenta que, en presencia de una operación de fusión, una expresión abreviada como C^
o C^^
puede no devolver el resultado que esperas, como ocurriría si estuvieras trabajando en una rama con un historial de confirmaciones lineal. La Figura 4-3 ilustra que C^^
no es lo mismo que C^2
.
Combinando un ref
e instancias de carets y tildes, se pueden seleccionar commits arbitrarios del gráfico de commit ancestral de ref
. Recuerda, sin embargo, que estos nombres son relativos al valor actual de ref
. Si se realiza una nueva confirmación sobre ref
el gráfico de confirmaciones se modifica con una nueva generación, y cada nombre "padre" se desplaza más atrás en el historial y en el gráfico.
Aquí tienes un ejemplo del propio historial de Git cuando su rama main
estaba en el commit a5828ae6b52137b913b978e16cd2334482eb4c1f
. Utilizando el comando
git show-branch --more=25
y limitando la salida a las 25 líneas finales, puedes inspeccionar el historial del gráfico y examinar una estructura compleja de fusión de ramas:
$git reset --hard a5828ae6b52137b913b978e16cd2334482eb4c1f
HEAD is now at a5828ae6b5 Git 2.31 $git show-branch --more=25
[main] Git 2.31 [main^] Merge branch 'jn/mergetool-hideresolved-is-optional' [main^^2] doc: describe mergetool configuration in git-mergetool(1) [main^^2^] mergetool: do not enable hideResolved by default [main^^2~2] mergetool: add per-tool support and overrides for the hideResolved flag [main~2] Merge branch 'tb/pack-revindex-on-disk' [main~2^2] pack-revindex.c: don't close unopened file descriptors [main~3] Merge tag 'l10n-2.31.0-rnd2' of git://github.com/git-l10n/git-po [main~3^2] l10n: zh_CN: for git v2.31.0 l10n round 1 and 2 [main~3^2^] Merge branch 'master' of github.com:vnwildman/git [main~3^2^^2] l10n: vi.po(5104t): for git v2.31.0 l10n round 2 [main~3^2~2] Merge branch 'l10n/zh_TW/210301' of github.com:l10n-tw/git-po [main~3^2~2^2] l10n: zh_TW.po: v2.31.0 round 2 (15 untranslated) [main~3^2~3] Merge branch 'po-id' of github.com:bagasme/git-po [main~3^2~3^2] l10n: Add translation team info [main~3^2~4] Merge branch 'master' of github.com:Softcatala/git-po [main~3^2~4^2] l10n: Update Catalan translation [main~3^2~5] Merge branch 'russian-l10n' of github.com:DJm00n/git-po-ru [main~3^2~5^2] l10n: ru.po: update Russian translation [main~3^2~6] Merge branch 'pt-PT' of github.com:git-l10n-pt-PT/git-po [main~3^2~6^2] l10n: pt_PT: add Portuguese translations part 1 [main~3^2~7] l10n: de.po: Update German translation for Git v2.31.0 [main~4] Git 2.31-rc2 [main~5] Sync with Git 2.30.2 for CVE-2021-21300 [main~5^2] Git 2.30.2 [main~6] Merge branch 'jt/transfer-fsck-across-packs-fix' $git rev-parse main~3^2~2^
8278f870221711c2116d3da2a0165ab00368f756
Entre main~3
y main~4
, tuvo lugar una fusión que introdujo un par de fusiones más, así como una simple confirmación llamada main~3^2~2^2
. Este es el commit 8278f870221711c2116d3da2a0165ab00368f756
.
El comando git rev-parse
es la autoridad final a la hora de traducir cualquier forma de nombre de confirmación -etiqueta, relativa, acortada o absoluta- en un ID hash de confirmación real y absoluto dentro de la base de datos de objetos.
Historia del compromiso
El comando principal para mostrar el historial de confirmaciones es git log
. Tiene más opciones, parámetros, campanas, silbatos, coloreadores, selectores, formateadores y chorradas que el legendario ls
. Pero no te preocupes. Al igual que con ls
, no necesitas aprenderte todos los detalles de inmediato. A continuación, nos sumergiremos en los recovecos del historial de confirmaciones de un repositorio.
Ver commits antiguos
Cuando ejecutes el comando git log
, la salida incluirá cada confirmación asociada y sus mensajes de registro en tu historial de confirmaciones, accesible desde el punto de inicio especificado.
Como ejemplo, si ejecutas el comando git log
sin ninguna opción adicional, es lo mismo que ejecutar el comando git log HEAD
. El resultado será una salida de todas las confirmaciones empezando por la confirmación HEAD
y todas las confirmaciones alcanzables hacia atrás a través del gráfico de confirmaciones (los gráficos de confirmaciones se tratarán en la siguiente sección). Por defecto, los resultados se muestran en orden cronológico inverso, pero ten en cuenta que, cuando Git recorre tu historial de confirmaciones, el orden inverso se ajusta al gráfico de confirmaciones y no al momento en que se tomó la instantánea.
Ser explícito en el punto de inicio de la salida del registro utilizando el comando git log commit
puede ser útil para ver el historial de una rama. Utilicemos el comando para ver un ejemplo de salida del propio repositorio Git:
$ git log main -2
commit 30cc8d0f147546d4dd77bf497f4dec51e7265bd8 (HEAD -> main, ...)
Author: Junio C Hamano <gitster@pobox.com>
Date: Sat Jul 2 17:01:34 2022 -0700
A regression fix for 2.37
Signed-off-by: Junio C Hamano <gitster@pobox.com>
commit 0f0bc2124b25476504e7215dc2af92d5748ad327
Merge: e4a4b31577 4788e8b256
Author: Junio C Hamano <gitster@pobox.com>
Date: Sat Jul 2 21:56:08 2022 -0700
Merge branch 'js/add-i-delete'
Rewrite of "git add -i" in C that appeared in Git 2.25 didn't
correctly record a removed file to the index, which was fixed.
* js/add-i-delete:
add --interactive: allow `update` to stage deleted files
La información de registro es una fuente autorizada de la verdad. Sin embargo, retroceder por todo el historial de confirmaciones de un gran repositorio probablemente no sea muy práctico ni significativo. Normalmente, un rango limitado del historial es más informativo y más fácil de trabajar.
Una forma de limitar el historial es especificar un rango de confirmaciones, una técnica que veremos más adelante en este capítulo. Puedes limitar el rango del historial utilizando la forma since
..
until
. Dado un rango, git log
muestra todas las confirmaciones posteriores a since
y que pasan por until
. También puedes especificar un recuento como lugar natural para empezar, por ejemplo, git log -3
.
He aquí un ejemplo:
$ git log --pretty=short --abbrev-commit main~9..main~7
commit be7935ed8b
Author: Junio C Hamano <gitster@pobox.com>
Merged the open-eintr workaround for macOS
commit 58d581c344
Author: Elijah Newren <newren@gmail.com>
Documentation/RelNotes: improve release note for rename detection work
Aquí, git log
muestra las confirmaciones entre main~9
y main~7
, o la séptima y octava confirmaciones anteriores en la rama principal. Aprenderás sobre rangos en "Rangos de commit".
En el ejemplo, hemos introducido dos opciones de formato , --pretty=short
y --abbrev-commit
. La primera ajusta la cantidad de información sobre cada confirmación y tiene diversas variaciones, como oneline
, short
, medium
, y full
, por nombrar algunas. Esta última simplemente solicita que se abrevien los ID hash SHA1.
También puedes utilizar la opción format:string
para especificar cómo se personaliza y muestra la información del registro.
He aquí un ejemplo:
$ git log --pretty=format:"%an was the author of commit %h, %ar with%nthe commit titled: [%s]%n" \
> --abbrev-commit main~9..main~7
Junio C Hamano was the author of commit be7935ed8b, 12 days ago with
the commit titled: [Merged the open-eintr workaround for macOS]
Elijah Newren was the author of commit 58d581c344, 12 days ago with
the commit titled: [Documentation/RelNotes: improve release note for rename detection work]
Puedes proporcionar la opción -n
junto con el comando git log
para limitar la salida a un máximo de n
confirmaciones. Esto restringe la salida según el número especificado. Además, combinando la opción -p
se imprimirá el parche, o los cambios introducidos por la confirmación, al tiempo que se limitan los conjuntos de resultados. Esto te ayuda a obtener más detalles sobre una confirmación proporcionando más contexto.
He aquí un ejemplo:
$ git log -1 -p 4fe86488
commit 4fe86488e1a550aa058c081c7e67644dd0f7c98e
Author: Jon Loeliger <jdl@freescale.com>
Date: Wed Apr 23 16:14:30 2008 -0500
Add otherwise missing --strict option to unpack-objects summary.
Signed-off-by: Jon Loeliger <jdl@freescale.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
diff --git a/Documentation/git-unpack-objects.txt b/Documentation/git-unpack-objects.txt
index 3697896..50947c5 100644
--- a/Documentation/git-unpack-objects.txt
+++ b/Documentation/git-unpack-objects.txt
@@ -8,7 +8,7 @@ git-unpack-objects - Unpack objects from a packed archive
SYNOPSIS
--------
-'git-unpack-objects' [-n] [-q] [-r] <pack-file
+'git-unpack-objects' [-n] [-q] [-r] [--strict] <pack-file
Si quieres saber qué archivos se modificaron en una confirmación junto con un recuento de cuántas líneas se modificaron en cada archivo, la opción --stat
será tu opción.
He aquí un ejemplo:
$ git log --pretty=short --stat main~9..main~7
commit be7935ed8bff19f481b033d0d242c5d5f239ed50
Author: Junio C Hamano <gitster@pobox.com>
Merged the open-eintr workaround for macOS
Documentation/RelNotes/2.31.0.txt | 5 +++++
1 file changed, 5 insertions(+)
commit 58d581c3446cb616b216307d6b47539bccd494cf
Author: Elijah Newren <newren@gmail.com>
Documentation/RelNotes: improve release note for rename detection work
Documentation/RelNotes/2.31.0.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Gráficos de compromiso
Hasta ahora hemos estado utilizando el comando git log
con opciones que muestran los resultados en un formato lineal. Aunque es útil comprender el historial de confirmaciones que conduce a un determinado punto en la línea temporal de un proyecto, no siempre queda claro en esta vista aplanada que dos confirmaciones consecutivas pueden no pertenecer a una misma rama.
La opción --graph
utilizada en combinación con el comando git log
imprime una representación textual del historial de confirmaciones del repositorio. En esta vista, puedes visualizar las bifurcaciones de una confirmación y el punto en el que se fusionan las ramas en la línea temporal del repositorio.
A continuación se muestra un historial simplificado de confirmaciones del código fuente de Git desde sus inicios:
$ git log 89d21f4b649..0a02ce72d9 --oneline --graph
* 0a02ce72d9 Clean up the Makefile a bit.
* 839a7a06f3 Add the simple scripts I used to do a merge with content conflicts.
* b51ad43140 Merge the new object model thing from Daniel Barkalow
|\
| * b5039db6d2 [PATCH] Switch implementations of merge-base, port to parsing
| * ff5ebe39b0 [PATCH] Port fsck-cache to use parsing functions
| * 5873b67eef [PATCH] Port rev-tree to parsing functions
| * 175785e5ff [PATCH] Implementations of parsing functions
| * 6eb8ae00d4 [PATCH] Header files for object parsing
* | a4b7dbef4e [PATCH] fix bug in read-cache.c which loses files when merging...
* | 1bc992acac [PATCH] Fix confusing behaviour of update-cache --refresh on...
* | 6ad6d3d36c Update README to reflect the hierarchical tree objects...
* | 64982f7510 [PATCH] (resend) show-diff.c off-by-one fix
* | 75118b13bc Pass a "merge-cache" helper program to execute a merge on...
* | 74b2428f55 [PATCH] fork optional branch point normazilation
* | d9f98eebcd Ignore any unmerged entries for "checkout-cache -a".
* | 5e5128ed1c Remove extraneous ',' ';' and '.' characters from...
* | 08ca0b04ba Make the revision tracking track the object types too.
* | d0d7cbe730 Make "commit-tree" check the input objects more carefully.
* | 7d60ad7cc9 Make "parse_commit" return the "struct revision" for the commit.
|/
* 6683463ed6 Do a very simple "merge-base" that finds the most recent...
* 15000d7899 Make "rev-tree.c" use the new-and-improved "mark_reachable()"
* 01796b0e91 Make "revision.h" slightly better to use.
Nota
La opción --oneline
es una abreviatura de --pretty=oneline
--abbrev-commit
utilizada conjuntamente.
El historial de confirmaciones del repositorio suele visualizarse como un gráfico3 como referencia cuando se habla de determinados comandos Git. La mayoría de las veces está relacionado con operaciones que pueden modificar el historial de confirmaciones del repositorio. Hablaremos de la reescritura de historiales de confirmaciones en el Capítulo 8.
Si recuerdas, en la "Visualización del almacén de objetos Git ", incluimos una figura para ayudarte a visualizar la disposición y la relación entre los objetos almacenados en el almacén de objetos Git. Esa figura se reproduce aquí como Figura 4-4.
Si tuviéramos que trazar el historial de confirmaciones de un repositorio utilizando la Figura 4-4, incluso para un repositorio pequeño con sólo un puñado de confirmaciones, fusiones y parches, ese mapa sería demasiado difícil de representar con este tipo de detalle. La Figura 4-5 muestra un gráfico de confirmaciones más completo, aunque algo simplificado, en el mismo formato. Imagina cómo se vería si se representaran todas las confirmaciones y todas las estructuras de datos.
La Figura 4-6 muestra el mismo gráfico de confirmación que la Figura 4-5, pero sin representar los objetos árbol y blob. Los nombres de las ramas también se muestran en los gráficos de confirmación como referencia de apoyo.
Podemos simplificar aún más la Figura 4-5 con una observación importante sobre las confirmaciones: cada confirmación introduce un objeto árbol que hace referencia a uno o más objetos blob y representa el estado completo del repositorio cuando se hizo una instantánea de la confirmación. Por lo tanto, una confirmación puede representarse sólo como un nombre, lo que simplifica enormemente el esquema del historial de confirmaciones del repositorio.
Las figuras 4-5 y 4-6 son ejemplos de un grafo acíclico dirigido (DAG). Un DAG tiene las siguientes propiedades importantes:
-
Todos los perímetros del gráfico están dirigidos de un nodo a otro.
-
Partiendo de cualquier nodo del grafo, no existe ningún camino a lo largo de los perímetros dirigidos que conduzca de nuevo al nodo inicial.
Git implementa el historial de confirmaciones dentro de un repositorio como un DAG. En el grafo de confirmaciones, cada nodo es una única confirmación, y todos los perímetros se dirigen de un nodo descendiente a otro nodo padre, formando una relación de ancestro. Los nodos de confirmación individuales suelen etiquetarse como se muestra en la Figura 4-7, y se utilizan para describir la historia de las confirmaciones y la relación entre ellas.
Una faceta importante de un DAG es que A Git no le importa la hora ni la temporización (absoluta o relativa) de las confirmaciones. La marca de tiempo real de una confirmación puede ser engañosa porque el reloj de un ordenador puede estar ajustado de forma incorrecta o incoherente. En un entorno de desarrollo distribuido, el problema se agrava. Lo que es cierto, sin embargo, es que si la confirmación Y
apunta al padre X
, entonces X
captura el estado del repositorio anterior al estado del repositorio de la confirmación Y
, independientemente de las marcas de tiempo que pueda haber en las confirmaciones.
Partiendo de esa noción, la Figura 4-7 muestra lo siguiente:
-
El tiempo es aproximadamente de izquierda a derecha.
-
A
es la confirmación raíz porque no tiene padre, yB
se produjo después deA
. -
Tanto
E
comoC
se produjeron después deB
, pero no se puede afirmar nada sobre el momento relativo entreC
yE
; cualquiera de los dos podría haberse producido antes que el otro. -
Los commits
E
yC
comparten un padre común,B
. Por tanto,B
es el origen de una rama. -
La rama
main
comienza con los commitsA
,B
,C
, yD
. -
Mientras tanto, la secuencia de confirmaciones
A
,B
,E
,F
, yG
forma la rama denominadapr-17
. La ramapr-17
apunta a la confirmaciónG
, como se explica en el Capítulo 3. -
La confirmación
H
es una confirmación de fusión, en la que la ramapr-17
se ha fusionado con la ramamain
. La operación de fusión se trata con más detalle en el Capítulo 6. -
Como se trata de una fusión,
H
tiene más de un commit padre, en este caso,D
yG
. -
Una vez realizada esta confirmación,
main
se actualizará para hacer referencia a la nueva confirmaciónH
, peropr-17
seguirá haciendo referencia aG
.
Con el tiempo, a medida que aprendas y consultes muchos diagramas DAG de confirmaciones, pronto notarás un patrón recurrente:
-
Las confirmaciones normales tienen exactamente un padre, que es la confirmación anterior en el historial. Cuando haces un cambio, tu cambio es la diferencia entre tu nueva confirmación y su padre.
-
Normalmente sólo hay una confirmación con cero padres: la confirmación raíz.
-
Una confirmación de fusión tiene más de una confirmación padre.
-
Un compromiso con más de un hijo es el lugar donde la historia empezó a divergir y formó una nueva rama.
En la práctica, se considera que los detalles de las confirmaciones intermedias carecen de importancia. Además, a menudo se elude el detalle de la implementación de una confirmación que apunta a su padre, como se muestra en la Figura 4-8.
El tiempo sigue siendo vagamente de izquierda a derecha, se muestran dos ramas y hay una confirmación de fusión identificada (H
), pero los perímetros dirigidos reales están simplificados porque se entienden implícitamente.
Los gráficos de confirmaciones son una representación bastante abstracta del historial real de confirmaciones, a diferencia de las herramientas que proporcionan representaciones concretas de los gráficos del historial de confirmaciones. Con estas herramientas, sin embargo, el tiempo suele representarse de abajo a arriba, de la confirmación más antigua a la más reciente. Conceptualmente, se trata de la misma información. En la siguiente sección hablaremos brevemente de la herramienta de libre distribución gitk
.4 Además de gitk
, existen otras herramientas en versión de pago o gratuita.
Utilizar gitk para ver el gráfico de confirmaciones
Un gráfico de confirmaciones puede ayudarte a visualizar una estructura y una relación complicadas. El comando gitk
puede dibujar un DAG de un repositorio representando el historial de confirmaciones del repositorio.
Veamos un ejemplo de repositorio sencillo con dos ramas y confirmaciones simples para añadir archivos:
$mkdir commit-graph-repo
# Operations to add new files, create new branch and merge branch ... ... $gitk
El programa gitk
puede hacer muchas cosas, pero centrémonos por ahora en el DAG. La salida del gráfico se parece a la Figura 4-9.
Rangos de compromiso
El comando git log
opera sobre una serie de confirmaciones y es uno de los muchos comandos que te permiten recorrer el historial de confirmaciones de tu repositorio. Para ser precisos, cuando especificas el commit Y
como punto de partida a git log
, en realidad estás solicitando a Git que muestre el registro de todos los commits alcanzables desde el commit Y
.
En un grafo de confirmaciones de Git, el conjunto de confirmaciones alcanzables es el conjunto de confirmaciones a las que puedes llegar desde una confirmación dada recorriendo los enlaces padre dirigidos. Conceptualmente y en términos de flujo de datos, el conjunto de confirmaciones alcanzables es el conjunto de confirmaciones antepasadas que fluyen hacia una confirmación inicial dada y contribuyen a ella.
Nota
En teoría de grafos, se dice que un nodo X es alcanzable desde otro nodo A si puedes empezar en A, recorrer los arcos del grafo según las reglas y llegar a X. El conjunto de nodos alcanzables para el nodo A es la colección de todos los nodos alcanzables desde A.
Existen varias opciones en Git que te permiten incluir y excluir confirmaciones dentro de un rango especificado. Estas opciones no se limitan al comando git log
; también son aplicables a otros subcomandos de Git compatibles. Por ejemplo, puedes excluir una confirmación específica X
y todas las confirmaciones accesibles desde X
con la expresión ^X
. Normalmente, se utiliza un rango para examinar una rama o parte de una rama.
Un rango se denota con una noción de doble período (..
), como en start..end
donde start
y end
pueden especificarse como se describe en "Identificación de commits".
Un rango de confirmaciones, start..end
se define como el conjunto de confirmaciones incluidas en end
y excluyente de start
. Normalmente esto se simplifica a la frase "en end
pero no start
." La Figura 4-11 ofrece una explicación ilustrada.
En "Visualización de confirmaciones antiguas", viste cómo utilizar un rango de confirmaciones con git log
. En el ejemplo se utilizó el rango main~9..main~7
para especificar la octava y la séptima confirmaciones anteriores en la rama principal. Para visualizar el rango, considera el gráfico de confirmaciones de la Figura 4-10. La rama M
se muestra sobre una parte de su historial de confirmaciones que es lineal.
Recuerda que el tiempo fluye de izquierda a derecha, por lo que M~11
es la confirmación más antigua mostrada, M~6
es la confirmación más reciente mostrada y A
es la octava confirmación anterior.
El rango M~9..M~7
representa dos commits, el octavo y el séptimo más antiguos, que se etiquetan como A
y B
. El rango no incluye M~9
(recuerda la frase "en M~7
pero no en M~9
").
Volviendo a la serie de confirmaciones del ejemplo anterior, vista como una operación de conjunto, así es como M~9..M~7
especifica sólo dos confirmaciones, A
y B
:
-
Comienza con todo lo que lleva a
M~7
, como se muestra en la primera línea de la Figura 4-11.-
Encuentra todo lo que lleva hasta
M~9
, incluido, como se muestra en la segunda línea de la figura.-
Resta
M~9
deM~7
para obtener los commits que se muestran en la tercera línea de la figura.
-
-
Cuando el historial de tu repositorio es una simple serie lineal de confirmaciones, es bastante fácil entender cómo funciona un rango. Pero cuando en el gráfico intervienen ramas o fusiones, las cosas pueden complicarse un poco, por lo que es importante entender la definición rigurosa.
Veamos algunos ejemplos más. Ten en cuenta que estos ejemplos son sólo representaciones abstractas y están diseñados para ser sencillos y fáciles de comprender. La operación merge
se utiliza para apoyar el concepto, y sus aspectos técnicos se describirán en detalle en el Capítulo 6.
Empezaremos con el caso de una rama main
con una historia lineal, como se muestra en la Figura 4-12. Los conjuntos B..E
, ^B E
, y C
, D
, y E
son equivalentes.
En la Figura 4-13, la rama main
en el commit V
se fusionó con la rama feature
en B
.
El rango feature..main
representa los commits de main
pero no los de feature
. Dado que cada commit de la rama main
anterior e incluido V
(es decir, el conjunto {..., T
, U
, V
}) contribuye a feature
, esos commits se excluyen, dejando W
, X
, Y
, y Z
.
El ejemplo inverso al anterior se muestra en la Figura 4-14. Aquí, feature
se ha fusionado con main
.
En este ejemplo, el rango feature..main
, que representa de nuevo las confirmaciones en main
pero no en feature
, es el conjunto de confirmaciones en la rama main
que conducen e incluyen V
, W
, X
, Y
y Z
.
Sin embargo, tenemos que ser un poco cuidadosos y considerar la historia completa de la rama feature
. Considera el caso en el que originalmente comenzó como una rama de main
y luego se fusionó de nuevo, como se muestra en la Figura 4-15.
En este caso, feature..main
contiene sólo los commits W
, X
, Y
, y Z
. Recuerda que el rango excluirá todos los commits alcanzables (yendo hacia atrás o hacia la izquierda en el gráfico) desde feature
(es decir, los commits D
, C
, B
, A
, y anteriores), así como V
, U
, y anteriores del otro padre de B
. El resultado es sólo de W
a Z
.
Por último, al igual que start..end
puede considerarse que representa una operación de resta de conjuntos, la notación A…B
(utilizando tres puntos) representa la diferencia simétrica entre A
y B
o el conjunto de confirmaciones alcanzables desde A
o B
pero no desde ambos. Debido a la simetría de la función, ninguno de los dos commits puede considerarse realmente un inicio o un final. En este sentido A
y B
son iguales.
Más formalmente, el conjunto de revisiones de la diferencia simétrica entre A
y B
, A…B
viene dado por lo siguiente
$ git rev-list A
B
--not $(git merge-base --all A
B
)
Veamos el ejemplo de la Figura 4-16.
Los commits que contribuyen a main
son (I
, H
, . . . , B
, A
, W
, V
, U
). Los commits que contribuyen a dev
son (Z
, Y
, . . . , U
, C
, B
, A
).
La unión de esos dos conjuntos es (A
, . . . , I
, U
, . . . , Z
). La base de fusión entre main
y dev
es el commit W
. En casos más complejos, podría haber múltiples bases de fusión, pero aquí sólo tenemos una. Los commits que contribuyen a W
son (W
, V
, U
, C
, B
, y A
); esos son también los commits que son comunes a main
y dev
, por lo que hay que eliminarlos para formar la diferencia simétrica: (I
, H
, Z
, Y
, X
, G
, F
, E
, D
).
Puede ser útil pensar en la diferencia simétrica entre dos ramas, A y B, como "mostrar todo en la rama A
o en la rama B
pero sólo hasta el punto en que las dos ramas divergieron".
Podemos calcular cada parte de la definición de diferencia simétrica:
A...B = (A OR B) AND NOT (merge-base --all A B)
Ahora que hemos descrito qué son los rangos de confirmaciones, cómo escribirlos y cómo funcionan, es importante revelar que Git en realidad no admite un verdadero operador de rango. Es puramente una conveniencia notacional que A..B
representa la forma subyacente ^A B
. En realidad, Git permite una manipulación del conjunto de confirmaciones mucho más potente en su línea de comandos. Los comandos que aceptan un rango en realidad están aceptando una secuencia arbitraria de confirmaciones incluidas y excluidas. Por ejemplo, podrías utilizar
$ git log ^dev ^feature ^bugfix main
para seleccionar los commits en main
pero no en ninguna de las ramas dev
, feature
, o bugfix
.
Una vez más, todos estos ejemplos pueden ser un poco abstractos, pero el poder de la representación de rangos sale realmente a la luz cuando consideras que cualquier nombre de rama puede utilizarse como parte del rango. Como se describe en "Seguimiento de ramas", si una de tus ramas representa los commits de otro repositorio, ¡puedes descubrir rápidamente el conjunto de commits que están en tu repositorio y no están en otro repositorio!
Resumen
Empezamos este capítulo presentando las confirmaciones como una unidad de cambio registrada, explicando además la importancia de que las confirmaciones se registren como un conjunto de cambios atómico. A continuación, hemos desarrollado gradualmente tu dominio de las confirmaciones, presentándote primero las distintas formas en que se puede identificar una confirmación. Hicimos hincapié en la importancia de aprender a identificar confirmaciones, ya que es la base de muchos comandos y conceptos intermedios y avanzados de Git que te ayudarán a enfrentarte con confianza a cualquier requisito complejo que pueda surgirte al tratar con repositorios en futuros proyectos. También hablamos de cómo puedes ver el historial de confirmaciones de un repositorio junto con métodos para restringir eficazmente un rango o conjunto del historial para comprender mejor cómo un repositorio llegó a su estado actual. Entender los rangos de confirmaciones puede ser complicado al principio, pero serás capaz de comprender la idea general cuando empieces a analizar el historial de confirmaciones de un repositorio con casos de uso específicos, especialmente aquellos que requieren que recorras el historial de confirmaciones hasta un estado concreto del repositorio.
1 Git también registra una bandera de modo que indica la ejecutabilidad de cada archivo. Los cambios en esta bandera también forman parte de un conjunto de cambios.
2 Sí, de hecho puedes introducir varios commits raíz en un único repositorio. Esto ocurre, por ejemplo, cuando se juntan dos proyectos diferentes y sus repositorios completos y se fusionan en uno solo.
3 Un grafo es una colección de nodos y un conjunto de perímetros entre los nodos. Comúnmente, se utiliza el diagrama de grafos acíclicos dirigidos (DAG) cuando se explica el historial de confirmaciones de Git.
4 El comando gitk
no es un git
subcomando; es un comando independiente y un paquete instalable.
Get Control de versiones con Git, 3ª edición now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.