CLASE 04
Git Internals
ReferenciasPuedes usar git log 1a410e para echar un vistazo a lo largo de toda tu historia, recorriendo y encontrando todos los objetos.
Pero para ello necesitas recordar que el ultimo commit es 1a410e.
En Git, esto es lo que se conoce como “referencias” o “refs”,
en la carpeta .git/refs puedes encontrar archivos con valores checksum SHA-1
y nombres simples que puedes usar en lugar de esos strings largos de checksums.
En el proyecto actual, la carpeta aun no contiene archivos:
find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
find .git/refs -type f
Para crear una nueva referencia que te sirva de ayuda para recordar cual es tu ultimo commit es tan simple como:
echo "1a410efbd13591db07496d4f82dfc14aad14979" > .git/refs/heads/master
A partir de ese momento puedes usar esa referencia en lugar del valor checksum SHA-1:
git log --pretty=oneline master # historial en una linea para la referencia "master"
1a410efbd13591db07496d4f82dfc14aad14979 third commit
cac0cab538b970a346d349e0d0e4196f5f3b1955 second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
No es conveniente editar directamente los archivos de referencias.
Git suministra un comando mucho mas seguro para hacer esto: git update-ref:
git update-ref refs/heads/master 1a410efbd13591db07496d4f82dfc14aad14979
Esto es basicamente lo que es una rama en Git: un simple puntero o referencia a la cabeza de una linea de trabajo. Para crear una rama nueva desde el segundo commit:
git update-ref refs/heads/test cac0ca # actualizar referencia con el nombre corto del checksum
Y la rama contendra unicamente trabajo desde esa confirmacion hacia atras.
Cuando lanzas
git branch <nombre>, Git ejecuta internamentegit update-refpara anadir el checksum SHA-1 del ultimo commit de la rama en que te encuentras a cualquier nueva referencia que quieras crear.
¿Como sabe Git cual es el checksum SHA-1 del ultimo commit cuando ejecutas git branch <nombre>?
La respuesta es el archivo HEAD.
El archivo HEAD es una referencia simbolica a la rama donde te encuentras en cada momento.
A diferencia de una referencia normal, la referencia simbolica contiene un enlace a otra referencia en vez del valor checksum SHA-1 de un objeto.
cat .git/HEAD
ref: refs/heads/master
Si lanzas git checkout test, Git actualiza el archivo .git/HEAD:
cat .git/HEAD
ref: refs/heads/test
Cuando lanzas git commit, se crea un nuevo objeto commit teniendo como padre el commit al que referencia en este momento el HEAD.
Puedes leer y cambiar este archivo de forma segura con el comando git symbolic-ref:
git symbolic-ref HEAD
refs/heads/master
# ---
git symbolic-ref HEAD refs/heads/test
cat .git/HEAD
ref: refs/heads/test
Pero no puedes fijar una referencia simbolica fuera de
refs/. Ej:git symbolic-ref HEAD testno funcionara.
Las etiquetas funcionan de forma muy parecida a los commits, pero apuntan a un objeto de etiqueta en lugar de directamente a un commit. El objeto etiqueta contiene un marcador, una fecha, un mensaje y un enlace al commit al que apunta. Es como una referencia permanente que siempre apunta al mismo commit.
Como vimos anteriormente en el EXTRA 01, hay dos tipos: anotadas y ligeras. Para crear una etiqueta ligera:
git update-ref refs/tags/v1.0 cac0cab538b970a346d349e0d0e4196f5f3b1955
Para crear una etiqueta anotada, Git crea un objeto etiqueta y luego escribe una referencia apuntando al objeto en lugar de directamente al commit.
Puedes comprobarlo con -a:
git tag -a v1.1 1a410efbd13591db07496d4f82dfc14aad14979 -m "test tag"
El checksum SHA-1 del objeto creado:
cat .git/refs/tags/v1.1
9585191f37f7b0fb9444511119c5069a8c2e6f28
Si ejecutas git cat-file sobre ese checksum SHA-1,
veras que el objeto apunta a su vez al checksum SHA-1 del commit etiquetado:
git cat-file -p 9585191f37f7b0fb9444511119c5069a8c2e6f28
object 1a410efbd13591db07496d4f82dfc14aad14979
type commit
tag v1.1
tagger Scott Chacon <schacon@gmail.com> Mon Mar 17 21:52:11 2008
test tag
El objeto etiqueta no necesita apuntar necesariamente a un commit, puedes etiquetar cualquier tipo de objeto Git.
El tercer tipo de referencia que puedes observar es la referencia a un remoto.
Si añades un remoto y envias datos a el, Git almacenara en .git/refs/remotes/<nombre> el ultimo valor para cada rama presente en ese remoto.
Por ejemplo, si añades un remoto denominado origin y envias la rama master:
git remote add origin git@github.com:schacon/simplegit-progit.git
git push origin master
Puedes confirmar cual es la rama master en el remoto origin revisando el archivo .git/refs/remotes/origin/master.
Las referencias a remotos son distintas de las ramas normales: Git las trata como de solo lectura.
Puedes hacer git checkout a una, pero Git no actualizara HEAD hacia ella, de modo que nunca la actualizaras con un git commit.
Actuan simplemente como marcadores del ultimo estado conocido de cada rama en cada servidor remoto declarado.
Git Internals
PackfilesEn este momento, el repositorio de pruebas tiene 11 objetos: 4 blobs, 3 trees, 3 commits y 1 etiqueta.
find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1
Git comprime todos esos objetos con zlib, por lo que ocupan muy poco. Sin embargo, entre todos suman solamente 925 bytes.
Para demostrar lo interesante del formato, añadiremos el archivo repo.rb de la libreria Git, un archivo de codigo fuente de unos 22KB:
curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb # Descargar repo.rb
git add repo.rb
git commit -m 'added repo.rb'
Si revisas el tree resultante, podras observar el valor checksum SHA-1 del objeto blob correspondiente a repo.rb:
git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 033b4468fa6b2a9547a70d88d1bbe0e4f3c6b1b4 repo.rb
100644 blob a3f4d07325967247213ae12ccd6933e966fb5fb5 text.txt
Usemos cat-file para ver como de grande es:
git cat-file -s 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
22044
Ahora modifica un poco el archivo y comprueba lo que sucede:
echo '# testing' >> repo.rb
git commit -am 'modified repo a bit'
Revisando el tree creado por este ultimo commit, veras algo interesante: el objeto blob es ahora completamente diferente.
git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob b042a60ef7dff760008df33cee372b945b6e884e repo.rb # Antes empezaba con 033b44
100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt
Aunque solo has añadido una linea al final de un archivo que ya contenia 400 lineas, Git ha almacenado el resultado como un objeto completamente nuevo que ocupa 22KB.
git cat-file -s b042a60ef7dff760008df33cee372b945b6e884e
22054
Hasta ahora, Git ha estado almacenando cada version completa del archivo como un objeto independiente.
Esto se conoce como formato relajado (loose). En disco tienes dos versiones de repo.rb practicamente identicas ocupando 22KB cada una.
¿No seria practico si Git pudiera almacenar uno de ellos completo y luego solo las diferencias del segundo con respecto al primero?
Git puede hacerlo. El formato inicial es el formato relajado, pero Git suele agrupar varios de estos objetos en un unico archivo binario denominado empaquetador (packfile) para ahorrar espacio y hacer mas eficiente su almacenamiento.
Esto sucede cada vez que tienes demasiados objetos en formato relajado, cuando lo llamas manualmente con git gc, o antes de enviar cualquier cosa a un servidor remoto:
git gc # garbage collector
Counting objects: 18, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (18/18), done.
Total 18 (delta 3), reused 0 (delta 0)
Tambien podemos ejecutar
git gcen cualquier momento que nosotros queramos.
Tras esto, si miras los objetos presentes en la carpeta, veras que han desaparecido la mayoria de los que habia antes y han aparecido un par de objetos nuevos:
find .git/objects -type f
.git/objects/bd/9dbf5ae1a885ea15267232646202b5ae5fe37
.git/objects/d6/704ab6b4e5c015af25cfcd0812f550a9fa3e4
.git/objects/info/packs
.git/objects/pack/pack-978e03765c4f561011e6998c8de9e30080905b8b.idx
.git/objects/pack/pack-978e03765c4f561011e6998c8de9e30080905b8b.pack
Los objetos que permanecen sin empaquetar son aquellos no referenciados por ningun commit: en este caso, los ejemplos de prueba que creamos sueltos anteriormente.
El nuevo archivo empaquetador contiene todos los objetos eliminados del sistema de archivo. El indice es un archivo que contiene las posiciones de cada uno de esos objetos dentro del empaquetador, permitiendonos buscarlos rapidamente.
Lo interesante es que aunque los objetos originales ocupaban 22KB cada uno, el nuevo archivo empaquetador ocupa apenas 7KB. Empaquetando los objetos, has conseguido reducir a la mitad el uso de disco.
Puedes comprobarlo mirando en el interior del archivo empaquetador con el comando de fontaneria git verify-pack:
git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
2431da676938450a4d72e260db3bf7b0f587bbc1 commit 223 155 12
69bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit 214 152 167
80d02664cb23ed55b226516648c7ad5d0a3deb90 commit 214 145 319
...
...
d982c7cb2c2a972ee391a85da481fc1f9127a01d tree 6 17 1314 1 \
deef2e1b793907545e50a2ea2ddb5ba6c58c4506
3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree 8 19 1331 1 \
deef2e1b793907545e50a2ea2ddb5ba6c58c4506
0155eb4229851634a0f03eb265b69f5a2d56f341 tree 71 76 1350
83baae61804e65cc73a7201a7252750c76066a30 blob 10 19 1426
fa49b077972391ad58037050f2a75f74e3671e92 blob 9 18 1445
b042a60ef7dff760008df33cee372b945b6e884e blob 22054 5799 1463 # <- version 2
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 9 20 7262 1 \ # <- version 1
b042a60ef7dff760008df33cee372b945b6e884e # referencia a la version 2
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob 10 19 7282
non delta: 15 objects
chain length = 1: 3 objects
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack: ok
El objeto blob 033b4 (la primera version de repo.rb), tiene una referencia al blob b042a (la segunda version).
La 3ra columna refleja el tamaño del objeto dentro del paquete, podemos ver que b042a ocupa 22KB, pero 033b4 solo 9B.
Esto se debe a que lo mas probable es que queramos recuperar la version mas reciente del archivo, entonces Git se anticipa a esto usando la version sin diferencia para el mas reciente.
Git Internals
Mantenimiento y Recuperacion de DatosDe vez en cuando, Git ejecuta automaticamente el comando git gc --auto. La mayor parte del tiempo este comando no hace nada, ya que necesitas tener alrededor de 7.000 objetos sueltos o mas de 50 archivos empaquetadores para que Git arranque una limpieza real.
Puedes lanzarlo manualmente cuando quieras:
git gc --auto
Entre otras cosas, gc agrupa todos los objetos sueltos en empaquetadores, consolida los empaquetadores pequeños en uno grande,
elimina objetos no alcanzables por ningun commit, y actualiza alternativas como git stash.
gc tambien empaqueta tus referencias en un unico archivo eficiente:
find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1
Despues de ejecutar git gc, esos archivos desaparecen de refs/ y se consolidan en .git/packed-refs:
cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a346d349e0d0e4196f5f3b1955 refs/heads/experiment
ab1afef80fac8e34258959aa4e8e7b6e655e9e8b refs/heads/master
9585191f37f7b0fb9444511119c5069a8c2e6f28 refs/tags/v1.0
^1a410efbd13591db07496d4f82dfc14aad14979
Las lineas que comienzan con ^ indican que la etiqueta superior es anotada,
y esa linea es el commit al que apunta directamente.
En algun momento de tu trabajo con Git puede que pierdas accidentalmente un commit.
Generalmente esto ocurre porque fuerzas el borrado de una rama con trabajo en ella,
o porque haces un reset --hard moviendo a un commit anterior.
Supongamos que tienes este historial y haces reset a un commit anterior:
git log --pretty=oneline
ab1afef80fac8e34258959aa4e8e7b6e655e9e8b modified repo a bit
484a59275031909876a47e24d18424ca6e0c4bf5 added repo.rb
1a410efbd13591db07496d4f82dfc14aad14979 third commit
cac0cab538b970a346d349e0d0e4196f5f3b1955 second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
git reset --hard 1a410efbd13591db07496d4f82dfc14aad14979
Ahora has perdido los dos commits superiores. Hay dos formas de recuperarlos.
Mientras trabajas, Git registra silenciosamente el valor de HEAD cada vez que cambia en el reflog.
Puedes consultarlo con:
git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo a bit
484a592 HEAD@{2}: commit: added repo.rb
Puedes ver que perdiste los commits ab1afef y 484a592. Para recuperarlos, crea una nueva rama apuntando a ese commit:
git branch recover-branch ab1afef
git log --pretty=oneline recover-branch
ab1afef80fac8e34258959aa4e8e7b6e655e9e8b modified repo a bit
484a59275031909876a47e24d18424ca6e0c4bf5 added repo.rb
1a410efbd13591db07496d4f82dfc14aad14979 third commit
...
git fsckSi el reflog no tiene la informacion que necesitas (por ejemplo porque el gc ya lo limpio), puedes usar git fsck, que comprueba la integridad de tu base de datos y lista todos los objetos a los que no se puede llegar:
git fsck --full
Checking object directories: 100%
Checking connectivity: done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4 # Blob perdido
dangling commit ab1afef80fac8e34258959aa4e8e7b6e655e9e8b # <- Commit perdido
dangling tree aea790b9a58f6cf6f2804eeac9359346671de7f0 # Tree perdido
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293 # Blob perdido
El dangling commit es el commit que perdiste. Puedes recuperarla de la misma forma: creando una rama apuntando a ese checksum SHA-1.
Uno de los problemas mas comunes que puede ocurrir es que alguien añada por error un archivo muy grande al repositorio. Incluso si lo borras en el siguiente commit, seguira presente en el historial y Git tendra que clonarlo siempre.
Para encontrar el culpable, puedes inspeccionar el empaquetador y buscar el objeto mas pesado con verify-pack:
git verify-pack -v .git/objects/pack/pack-*.idx \ # Verifica el paquete
| sort -k 3 -n \ # Ordena por la 3ra columna, la de tamaño en Bytes
| tail -3 # Mostrar los ultimos 3 resultados
#
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438 # Blob de 4GB!
Luego averigua a que archivo corresponde ese checksum SHA-1 con rev-list:
git rev-list --objects --all | grep 82c99a3 # Listar todos los objetos, luego buscar por 82c99a3
82c99a3e86bb1267b236a90d7b697a8d07cc8b6d git.tgz
Para eliminar ese archivo de todo el historial, primero identifica en que commits aparecio:
git log --oneline --branches -- git.tgz # Lista los commits donde aparece git.tgz
dadf7258 oops - removed large tarball
7b30847d add git tarball
Luego reescribe todos los commits desde ese en adelante para que no contengan ese archivo con filter-branch:
git filter-branch --index-filter \
'git rm --ignore-unmatch --cached git.tgz' -- 7b30847d^..
Este comando revisa los commits desde el 7b30847d en adelante ^, revisa el area de preparacion --index-filter,
y ejecuta la orden de eliminacion git rm --cached git.tgz para quitar el archivo git.tgz.
El comando
filter-branchlo veremos mas adelante en Herramientas avanzadas pt.2.
Finalmente, limpia las referencias antiguas, el reflog y ejecuta gc para eliminar de verdad los objetos:
rm -Rf .git/refs/original
rm -Rf .git/logs/
git gc
Para verificar que el archivo grande ya no esta usamos count-objects:
git count-objects -v
count: 8
size: 2040
in-pack: 19
packs: 1
size-pack: 3
prune-packable: 0
garbage: 0
size-garbage: 0
El tamaño del empaquetador (
size-pack) es ahora de 3KB en lugar de los 3MB anteriores. El archivo grande ha desaparecido del historial.
Variables de Entorno
Referencia rapidaGit siempre se ejecuta dentro de un shell y utiliza variables de entorno para determinar como comportarse. Aqui las mas utiles para el dia a dia:
| Variable | Descripcion |
|---|---|
GIT_DIR | Cambia la ubicacion de la carpeta .git/ |
GIT_WORK_TREE | Cambia el directorio de trabajo del repositorio |
GIT_AUTHOR_NAME | Sobreescribe el nombre del autor del commit |
GIT_AUTHOR_EMAIL | Sobreescribe el correo del autor del commit |
GIT_COMMITTER_NAME | Sobreescribe el nombre de quien confirma |
GIT_COMMITTER_EMAIL | Sobreescribe el correo de quien confirma |
GIT_TRACE | Activa logging detallado para depuracion |
GIT_SSH | Binario alternativo a usar cuando Git necesita conectarse via SSH |
La referencia completa esta en el Cap. 10.8 de Pro Git.
10.3 a 10.8 (excepto 10.5 y 10.6, junto con un poco de 10.8)