Git Magic

Ben Lynn


Tabla de contenidos

Prólogo
Gracias!
Licencia
Hosting Git gratuito
1. Introducción
Trabajar Es Jugar
Control De Versiones
Control Distribuído
Una Tonta Superstición
Conflictos al fusionar
2. Trucos Básicos
Guardando Estados
Agrega, Elimina, Renombra
Deshacer/Rehacer Avanzado
Revirtiendo
Descargando Archivos
Lo Más Nuevo
Publicación Al Instante
Que es lo que hice?
Ejercicio
3. Clonando
Sincronizar Computadoras
Control Clásico de Fuentes
Bifurcando (fork) un proyecto
Respaldos Definitivos
Multitask A La Velocidad De La Luz
Control Guerrillero De Versiones
4. Magia Con Los Branches
La Tecla Del Jefe
Trabajo Sucio
Arreglos Rápidos
Flujo De Trabajo Ininterrumpido
Reorganizando Una Mezcla
Administrando branches
Branches Temporales
Trabaja como quieras
5. Lessons of History
I Stand Corrected
… And Then Some
Local Changes Last
Rewriting History
Making History
Where Did It All Go Wrong?
Who Made It All Go Wrong?
Personal Experience
6. Git Grandmastery
Source Releases
Changelog Generation
Git Over SSH, HTTP
Git Over Anything
Patches: The Global Currency
Sorry, We’ve Moved
Remote Branches
Multiple Remotes
Commit What Changed
My Commit Is Too Big!
Staged Changes
Don’t Lose Your HEAD
HEAD-hunting
Building On Git
Daring Stunts
7. Secrets Revealed
Invisibility
Integrity
Intelligence
Indexing
Bare Repositories
Git’s Origins
The Object Database
Blobs
Trees
Commits
Indistinguishable From Magic
8. Git Shortcomings
SHA1 Weaknesses
Microsoft Windows
Unrelated Files
Who’s Editing What?
File History
Initial Clone
Volatile Projects
Global Counter
Empty Subdirectories
Initial Commit
A. Translating This Guide

Prólogo

Tabla de contenidos

Gracias!
Licencia
Hosting Git gratuito

Git es la navaja suiza del control de versiones. Una herramienta de control de revisiones confiable, versátil y multipropósito, que por su extraordinaria flexibilidad es complicada de aprender, y más aún de dominar. Estoy documentando lo que he aprendido hasta ahora en estas páginas, porque inicialmente tuve dificultades para comprender el manual de usuario de Git.

Tal como observó Arthur C. Clarke, cualquier tecnología suficientemente avanzada, es indistinguible de la magia. Este es un gran modo de acercarce a Git: los novatos pueden ignorar su funcionamiento interno, y ver a Git como un artefacto que puede asombrar a los amigos y enfurecer a los enemigos con sus maravillosas habilidades.

En lugar de ser detallados, proveemos instrucciones generales para efectos particulares. Luego de un uso reiterado, gradualmente irás entendiendo como funciona cada truco, y como adaptar las recetas a tus necesidades.

Otras ediciones

Gracias!

Agradezco a Dustin Sallings, Alberto Bertogli, James Cameron, Douglas Livingstone, Michael Budde, Richard Albury, Tarmigan, Derek Mahar y Frode Aannevik por sugerencias y mejoras. Gracias a Daniel Baumann por crear y mantener el paquete para Debian. También gracias a JunJie, Meng y JiangWei por la traduccción al chino. [Si me olvidé de tí, por favor recuérdamelo, porque suelo olvidarme de actualizar esta sección]

Estoy muy agradecido por todos los que me han dado apoyo y elogios. Me gustaría que este fuera un libro real impreso, para poder citar sus generosas palabras en la tapa a modo de promoción. Hablando en serio, aprecio enormemente cada mensaje. El leerlos siempre ilumina mi ánimo.

Licencia

Esta guía se publica bajo la GNU General Public License versión 3. Naturalmente, los fuentes se guardan en un repositorio Git, y pueden ser obtenidos escribiendo:

$ git clone git://repo.or.cz/gitmagic.git  # Crea el directorio "gitmagic".

Ver debajo por otros mirrors.

Hosting Git gratuito

Capítulo 1. Introducción

Voy a usar una analogía para explicar el control de versiones. Mira el artículo de wikipedia sobre control de versiones para una explicación más cuerda.

Trabajar Es Jugar

He jugado juegos de PC casi toda mi vida. En cambio, empecé a usar sistemas de control de versiones siendo adulto. Sospecho que no soy el único, y comparar ambas cosas puede hacer que estos conceptos sean más fáciles de explicar y entender.

Piensa en editar tu código o documento, o lo que sea, como si fuera jugar un juego. Una vez que progresaste mucho, te gustaría guardar. Para lograrlo, haces click en el botón de "Guardar" en tu editor de confianza.

Pero esto va a sobreescribir tu versión antigua. Es como esos viejos juegos que solo tenían un slot para guardar: se podía guardar, pero nunca podías volver a un estado anterior. Esto era una pena, porque tu versión anterior podía haber estado justo en una parte que era particularmente divertida, y podías querer volver a jugarla algún día. O peor aún, tu partida actual está en un estado donde es imposible ganar, y tienes que volver a empezar.

Control De Versiones

Cuando estás editando, puedes "Guardar Como…" un archivo diferente, o copiar el archivo a otro lugar antes de guardar si quieres probar versiones viejas. También puedes usar compresión para ahorrar espacio. Esta es una forma primitiva y muy trabajosa de control de versiones. Los videojuegos han mejorado esto hace ya tiempo, muchas veces permitiendo guardar en varios slots, fechados automáticamente.

Hagamos que el problema sea un poco más complejo. Imagina que tienes un montón de archivos que van juntos, como el código fuente de un proyecto, o archivos para un sitio web. Ahora, si quieres mantener una vieja versión, debes archivar un directorio completo. Tener muchar versiones a mano es inconveniente y rápidamente se vuelve costoso.

Con algunos juegos, una partida guardada en realidad consiste de un directorio lleno de archivos. Estos videojuegos ocultan este detalle del jugador y presentan una interfaz conveniente para administrar diferentes versiones de este directorio.

Los sistemas de control de versiones no son diferentes. Todos tienen lindas interfaces para administrar un directorio de cosas. Puedes guardar el estado del directorio tantas veces como quieras, y tiempo después puedes cargar cualquiera de los estados guardados. A diferencia de la mayoría de los juegos, normalmente estos sistemas son inteligentes en cuanto la conservación del espacio. Por lo general, solo algunos pocos archivos cambian de versión a versión, y no es un gran cambio. Guardar las diferencias en lugar de nuevas copias ahorra espacio.

Control Distribuído

Ahora imagina un juego muy difícil. Tan difícil para terminar, que muchos jugadores experientes alrededor del mundo deciden agruparse e intercambiar sus juegos guardados para intentar terminarlo. Los "Speedruns" son ejemplos de la vida real: los jugadores se especializan en diferents niveles del mismo juego y colaboran para lograr resultados sorprendentes. ¿Cómo armarías un sistema para que puedan descargar las partidas de los otros de manera simple? ¿Y para que suban las nuevas?

Antes, cada proyecto usaba un control de versiones centralizado. Un servidor en algún lado contenía todos los juegos salvados. Nadie más los tenía. Cada jugador tenía a lo sumo un un par de juegos guardados en su máquina. Cuando un jugador quería progresar, obtenía la última versión del servidor principal, jugaba un rato, guardaba y volvía a subir al servidor para que todos los demás pudieran usarlo.

¿Qué pasa si un jugador quería obtener un juego anterior por algún motivo? Tal vez el juego actual está en un estado donde es imposible ganar, porque alguien olvidó obtener un objeto antes de pasar el nivel tres, por que que se quiere obtener el último juego guardado donde todavía es posible completarlo. O tal vez quieren comparar dos estados antiguos, para ver cuánto trabajo hizo un jugador en particular.

Puede haber varias razones para querer ver una revisión antigua, pero el resultado es siempre el mismo. Tienen que pedirle esa vieja partida al servidor central. Mientras mas juegos guardados se quieran, más se necesita esa comunicación.

La nueva generación de sistemas de control de versiones, de la cual Git es miembro, se conoce como sistemas distribuídos, y se puede pensar en ella como una generalización de sistemas centralizados. Cuando los jugadores descargan del servidor central, obtienen todos los juegos guardados, no solo el último. Es como si tuvieran un mirror del servidor central.

Esta operación inicial de clonado, puede ser cara, especialmente si el historial es largo, pero a la larga termina siendo mejor. Un beneficio inmediato es que cuando se quiere una versión vieja por el motivo que sea, la comunicación con el servidor es innecesaria.

Una Tonta Superstición

Una creencia popular errónea es que los sistemas distribuídos son poco apropiados para proyectos que requieren un repositorio central oficial. Nada podría estar más lejos de la verdad. Fotografiar a alguien no hace que su alma sea robada, clonar el repositorio central no disminuye su importancia.

Una buena aproximación inicial, es que cualquier cosa que se puede hacer con un sistema de control de versiones centralizado, se puede hacer mejor con un sistema de versiones distribuído que esté bien diseñado. Los recursos de red son simplemente más costosos que los recursos locales. Aunque luego veremos que hay algunas desventajas para un sistema distribuído, hay menos probabilidad de hacer comparaciones erroneas al tener esto en cuenta.

Un proyecto pequeño, puede necesitar solo una fracción de de las características que un sistema así ofrece. Pero, ¿usarías números romanos si solo necesitas usar números pequeños?. Además, tu proyecto puede crecer más allá de tus expectativas originales. Usar Git desde el comienzo, es como llevar una navaja suiza, aunque solo pretendas usarla para abrir botellas. El día que necesites desesperadamente un destornillador, vas a agradecer el tener más que un simple destapador.

Conflictos al fusionar

Para este tema, habría que estirar demasiado nuestra analogía con un videojuego. En lugar de eso, esta vez consideremos editar un documento.

Supongamos que Alice inserta una línea al comienzo de un archivo, y Bob agrega una línea al final de su copia. Ambos suben sus cambios. La mayoría de los sistemas automáticamente van a deducir un accionar razonable: aceptar y hacer merge (Nota del Traductor: fusionar en inglés) de los cambios, para que tanto la edición de Alice como la de Bob sean aplicadas.

Ahora supongamos que Alice y Bob han hecho ediciones distintas sobre la misma línea. Entonces es imposible resolver el conflicto sin intervención humana.Se le informa a la segunda persona en hacer upload que hay un conflicto de merge, y ellos deben elegir entre ambas ediciones, o cambiar la línea por completo.

Pueden surgir situaciones más complejas. Los sistemas de control de versiones manejan automáticamente los casos simples, y dejan los más complejos para los humanos. Usualmente este comportamiento es configurable.

Capítulo 2. Trucos Básicos

En lugar de sumergirte en un mar de comandos de Git, usa estos ejemplos elementales para mojarte los pies. A pesar de sus simplicidad, todos son útiles. De hecho, en mis primeros meses con Git nunca fui más allá del material en este capítulo.

Guardando Estados

Estás a punto de intentar algo drástico? Antes de hacerlo, toma una instantánea de todos los archivos en el directorio actual con:

$ git init
$ git add .
$ git commit -m "My first backup"

Ahora, si tu edición se vuelve irrecuperable, ejecuta:

$ git reset --hard

para volver a donde estabas. Para volver a salvar el estado:

$ git commit -a -m "Otro respaldo"

Agrega, Elimina, Renombra

El comando anterior solo seguirá la pista de los archivos que estaban presentes la primera vez que ejecutaste git add. Si añades nuevos archivos o subdirectorios, deberás decirle a Git:

$ git add ARCHIVOSNUEVOS...

De manera similar, si quieres que Git se olvide de determinados archivos, porque (por ejemplo) los borraste:

$ git rm ARCHIVOSVIEJOS...

Renombrar un archivo es lo mismo que eliminar el nombre anterior y agregar el nuevo. También puedes usar git mv que tiene la misma sintaxis que el comando mv. Por ejemplo:

$ git mv ARCHIVOVIEJO ARCHIVONUEVO

Deshacer/Rehacer Avanzado

Algunas veces solo quieres ir hacia atrás y olvidarte de todos los cambios a partir de cierto punto, porque estaban todos mal. Entonces:

$ git log

te muestra una lista de commits recientes, y sus hashes SHA1. A continuación, escribe:

$ git reset --hard SHA1_HASH

para recuperar el estado de un commit dado, y borrar para siempre cualquier recuerdo de commits más nuevos.

Otras veces, quieres saltar a un estado anterior temporalmente. En ese caso escribe:

$ git checkout SHA1_HASH

Esto te lleva atrás en el tiempo, sin tocar los commits más nuevos. Sin embargo, como en los viajes en el tiempo de las películas de ciencia ficción, estarás en una realidad alternativa, porque tus acciones fueron diferentes a las de la primera vez.

Esta realidad alternativa se llama branch (rama), y tendremos más cosas para decir al respecto luego. Por ahora solo recuerda que

$ git checkout master

te llevará al presente. También, para que Git no se queje, siempre haz un commit o resetea tus cambios antes de ejecutar checkout.

Para retomar la analogía de los videojuegos:

  • git reset \--hard: carga un juego viejo y borra todos los que son mas nuevos que el que acabas de cargar.
  • git checkout: carga un juego viejo, pero si continúas jugando, el estado del juego se desviará de los juegos que salvaste la primera vez. Cualquierpartido nuevo que guardes, terminará en una branch separada, representando la realidad alternativa a la que entraste. Luego nos encargaremos de esto

Puedes elegir el restaurar solo archivos o directorios en particular, al agregarlos al final del comando: You can choose only to restore particular files and subdirectories by appending them after the command:

$ git checkout SHA1_HASH algun.archivo otro.archivo

Ten cuidado, esta forma de checkout puede sobreescribir archivos sin avisar. Para prevenir accidentes, haz commit antes de ejecutar cualquier comando de checkout, especialmente cuando estás aprendiendo a usar Git. En general, cuando te sientas inseguro del resultado de una operación, sea o no de Git, ejecuta antes git commit -a.

¿No te gusta cortar y pegar hashes? Entonces usa:

$ git checkout :/"Mi primer r"

para saltar al commit que comienza con el mensaje dado.

También puedes pedir el 5to estado hacia atrás:

$ git checkout master~5

Revirtiendo

En una corte, los eventos pueden ser eliminados del registro. Igualmente, puedes elegir commits específicos para deshacer.

$ git commit -a
$ git revert SHA1_HASH

va a deshacer solo el commit con el hash dado. Ejecutar git log revela que el revert es registrado como un nuevo commit.

Descargando Archivos

Obtén una copia de un proyecto administrado por git escribiendo:

$ git clone git://servidor/ruta/a/los/archivos

Por ejemplo, para bajar todos los archivos que usé para crear este sitio:

$ git clone git://git.or.cz/gitmagic.git

Pronto tendremos más para decir acerca del comando clone.

Lo Más Nuevo

Si ya descargaste una copia de un proyecto usando git clone, puedes actualizarte a la última versión con:

$ git pull

Publicación Al Instante

Imagina que has escrito un script que te gustaría compartir con otros. Puedes decirles que simplemente lo bajen de tu computadora, pero si lo hacen mientras estás haciendo una modificación, pueden terminar en problemas. Es por esto que existen los ciclos de desarrollo. Los programadores pueden trabajar en un proyecto de manera frecuente, pero solo hacen público el código cuando consideran que es presentable.

Para hacer esto con Git, en el directorio donde guardas tu script:

$ git init
$ git add .
$ git commit -m "Primer lanzamiento"

Entonces puedes decirle a tus usuarios que ejecuten:

$ git clone tu.maquina:/ruta/al/script

para descargar tu script. Esto asume que tienen acceso por ssh. Si no es así, ejecuta git daemon y dile a tus usuarios que usen:

$ git clone git://tu.maquina/ruta/al/script

De ahora en más, cada vez que tu script esté listo para el lanzamiento, escribe:

$ git commit -a -m "Siguiente lanzamiento"

y tus usuarios puede actualizar su versión yendo al directorio que tiene tu script y ejecutando:

$ git pull

Tus usuarios nunca terminarán usando una versión de tu script que no quieres que vean. Obviamente este truco funciona para lo que sea, no solo scripts.

Que es lo que hice?

Averigua que cambios hiciste desde el último commit con:

$ git diff

O desde ayer:

$ git diff "@{yesterday}"

O entre una versión en particular y 2 versiones hacia atrás:

$ git diff SHA1_HASH "master~2"

En cado caso la salida es un patch (parche) que puede ser aplicado con git apply Para ver cambios desde hace 2 semanas, puedes intentar:

$ git whatchanged --since="2 weeks ago"

Usualmente recorro la historia con qgit , dada su interfaz pulida y fotogénica, o tig, una interfaz en modo texto que funciona bien a través conexiones lentas. Como alternativa, puedes instalar un servidor web, ejecutar git instaweb y utilizar cualquier navegador web.

Ejercicio

Siendo A, B, C, y D cuatro commits sucesivos, donde B es el mismo que A pero con algunos archivos eliminados. Queremos volver a agregar los archivos en D pero no en B. ¿Cómo podemos hacer esto?

Hay por lo menos tres soluciones. Asumiendo que estamos en D:

  1. La diferencia entre A y B son los archivos eliminados. Podemos crear un patch representando esta diferencia y aplicarlo:

    $ git diff B A | git apply
    
  2. Como en A tenemos los archivos guardados, podemos recuperarlos :

    $ git checkout A ARCHIVOS...
    
  3. Podemos ver el pasaje de A a B como un cambio que queremos deshacer:

    $ git revert B
    

¿Cuál alternativa es la mejor? Cualquiera que prefieras. Es fácil obtener lo que quieres con Git, y normalmente hay varias formas de hacerlo.

Capítulo 3. Clonando

En sistemas de control de versiones antiguos, checkout es la operación standard para obtener archivos. Obtienes un conjunto de archivos en estado guardado que solicistaste.

En Git, y otros sistemas de control de versiones distribuídos, clonar es la operación standard. Para obtener archivos se crea un clon de un repositorio entero. En otras palabras, practicamente se crea una copia idéntica del servidor central. Todo lo que se pueda hacer en el repositorio principal, también podrás hacerlo.

Sincronizar Computadoras

Este es el motivo por el que usé Git por primera vez. Puedo tolerar hacer tarballs o usar rsync para backups y sincronización básica. Pero algunas veces edito en mi laptop, otras veces en mi desktop, y ambas pueden no haberse comunicado en el medio.

Inicializa un repositorio de Git y haz haz commit de tus archivos en una máquina, luego en la otra:

$ git clone otra.computadora:/ruta/a/archivos

para crear una segunda copia de los archivos y el repositorio Git. De ahora en más,

$ git commit -a
$ git pull otra.computadora:/ruta/a/archivos HEAD

va a traer (pull) el estado de los archivos desde la otra máquina hacia la que estás trabajando. Si haz hecho cambios que generen conflictos en un archivo, Git te va a avisar y deberías hacer commit luego de resolverlos.

Control Clásico de Fuentes

Inicializa un repositorio de Git para tus archivos:

$ git init
$ git add .
$ git commit -m "Commit Inicial"

En el servidor central, inicializa un repositorio vacío de Git con algún nombre, y abre el Git daemon si es necesario:

$ GIT_DIR=proj.git git init
$ git daemon --detach  # podría ya estar corriendo

Algunos servidores publicos, como repo.or.cz, tienen un método diferente para configurar el repositorio inicialmente vacío de Git, como llenar un formulario en una página.

Empuja (push) tu proyecto hacia el servidor central con:

$ git push git://servidor.central/ruta/al/proyecto.git HEAD

Ya estamos listos. Para copiarse los fuentes, un desarrollador escribe:

$ git clone git://servidor.central/ruta/al/proyecto.git

Luego de hacer cambios, el código en envía al servidor central con:

$ git commit -a
$ git push

Si hubo actualizaciones en el servidor principal, la última versión debe ser traída antes de enviar lo nuevo. Para sincronizar con la última versión:

$ git commit -a
$ git pull

Bifurcando (fork) un proyecto

¿Harto de la forma en la que se maneja un proyecto?¿Crees que podrías hacerlo mejor? Entonces en tu servidor:

$ git clone git://servidor.principal/ruta/a/archivos

Luego avísale a todos de tu fork del proyecto en tu servidor.

Luego, en cualquier momento, puedes unir (merge) los cambios del proyecto original con:

$ git pull

Respaldos Definitivos

¿Quieres varios respaldos redundantes a prueba de manipulación y geográficamente diversos? Si tu proyecto tiene varios desarrolladores, ¡no hagas nada! Cada clon de tu código es un backup efectivo. No sólo del estado actual, sino que también de la historia completa de tu proyecto. Gracias al hashing criptográfico, si hay corrupción en cualquiera de los clones, va a ser detectado tan pronto como intente comunicarse con otros.

Si tu proyecto no es tan popular, busca tantos servidores como puedas para hospedar tus clones.

El verdadero paranoico debería siempre escribir el último hash SHA1 de 20-bytes de su HEAD en algún lugar seguro. Tiene que ser seguro, no privado. Por ejemplo, publicarlo en un diario funcionaría bien, porque es difícil para un atacante el alterar cada copia de un diario.

Multitask A La Velocidad De La Luz

Digamos que quieres trabajar en varias prestaciones a la vez. Haz commit de tu proyecto y ejecuta:

$ git clone . /un/nuevo/directorio

Git se aprovecha de los hard links y de compartir archivos de la manera mas segura posible para crear este clon, por lo que estará listo en un segundo, y podrás trabajar en dos prestaciones independientes de manera simultánea. Por ejemplo, puedes editar un clon mientras el otro está compilando.

En cualquier momento, puedes hacer commit y pull de los cambios desde el otro clon.

$ git pull /el/otro/clon HEAD

Control Guerrillero De Versiones

¿Estás trabajando en un proyecto que usa algún otro sistema de control de versiones y extrañas mucho a Git? Entonces inicializa un repositorio de Git en tu directorio de trabajo.

$ git init
$ git add .
$ git commit -m "Commit Inicial"

y luego clónalo:

$ git clone . /un/nuevo/directorio

Ahora debes trabajar en el nuevo directorio, usando Git como te sea más cómodo. Cada tanto, querrás sincronizar con los demás, en ese caso, ve al directorio original, sincroniza usando el otro sistema de control de versiones y escribe:

$ git add .
$ git commit -m "Sincronizo con los demás"

Luego ve al nuevo directorio y escribe:

$ git commit -a -m "Descripción de mis cambios"
$ git pull

El procedimiento para pasarle tus cambios a los demás depende de cuál es tu otro sistema de control de versiones. El nuevo directorio contiene los archivos con tus cambios. Ejecuta los comandos que sean necesarios para subirlos al repositorio central del otro sistema de control de versiones.

El comando git svn automatiza lo anterior para repositorios de Subversion, y también puede ser usado para exportar un proyecto de Git a un repositorio de Subversion.

Capítulo 4. Magia Con Los Branches

El hacer branches (ramificar) y merges (unir) de manera instantánea, son dos de las prestaciones más letales de Git.

Problema: Factores externos necesitan inevitablemente de cambios de contexto. Un bug severo se manifiesta en la última versión sin previo aviso. El plazo para alguna prestación se acorta. Un desarrollador que tiene que ayudar en una sección indispensable del proyecto está por tomar licencia. En cualquier caso, debes soltar abruptamente lo que estás haciendo y enfocarte en una tarea completamente diferente.

Interrumpir tu línea de pensamiento puede ser negativo para tu productividad, y cuanto más engorroso sea el cambiar contextos, mayor es la pérdida. Con los sistemas centralizados, debemos descargar una nueva copia. Los sistemas distribuídos se comportan mejor, dado que podemos clonar la versión deseada localmente.

Pero el clonar igual implica copiar todo el directorio junto con toda la historia hasta el momento. Aunque Git reduce el costousando hard links y el compartir archivos, los archivos del proyecto deben ser recreados enteramente en el nuevo directorio.

Solución: Git tiene una mejor herramienta para estas situaciones que es mucho más rápida y eficiente en tamaño que clonar git branch.

Con esta palabra mágica, los archivos en tu directorio se transforman súbitamente de una versión en otra. Esta transformación puede hacer más que simplemente ir hacia atrás o adelante en la historia. Tus archivos pueden mutar desde la última versión lanzada, a la versión experimental, a la versión en desarrollo, a la versión de un amigo y así sucesivamente.

La Tecla Del Jefe

¿Alguna vez jugaste uno de esos juegos donde con solo presionar un botón ("la tecla del jefe"), la pantalla inmediatamente muestra una hoja de cálculo o algo así? La idea es que si el jefe entra a la oficina mientras estás en el juego, lo puedes esconder rápidamente.

En algún directorio:

$ echo "Soy más inteligente que mi jefe" > miarchivo.txt
$ git init
$ git add .
$ git commit -m "Commit inicial"

Creamos un repositorio de Git que guarda un archivo de texto conteniendo un mensaje dado. Ahora escribe:

$ git checkout -b jefe  # nada parece cambiar luego de esto
$ echo "Mi jefe es más inteligente que yo" > miarchivo.txt
$ git commit -a -m "Otro commit"

Parecería que sobreescribimos nuestro archivo y le hicimos commit. Pero es una ilusión. Escribe:

$ git checkout master  # cambia a la versión original del archivo

y presto! El archivo de texto es restaurado. Y si el jefe decide investigar este directorio, escribimos:

$ git checkout jefe  # cambia a la versión adecuada para los ojos del jefe

Puedes cambiar entre ambas versiones del archivo cuantas veces quieras, y hacer commit en ambas de manera independiente.

Trabajo Sucio

Supongamos que estás trabajando en alguna prestación, y que por alguna razón, necesitas volver a una versión vieja y poner temporalmente algunos "print" para ver como funciona algo. Entonces:

$ git commit -a
$ git checkout SHA1_HASH

Ahora puedes agregar cualquier código temporal horrible por todos lados. Incluso puedes hacer commit de estos cambios. Cuando termines,

$ git checkout master

para volver a tu trabajo original. Observa que arrastrarás cualquier cambio del que no hayas hecho commit.

¿Que pasa si quisieras cambiar los cambios temporales? Facil:

$ git checkout -b sucio

y haz commit antes de volver a la branch master. Cuando quieras volver a los cambios sucios, simplemente escribe:

$ git checkout sucio

Mencionamos este comando en un capítulo anterior, cuando discutíamos sobre cargar estados antiguos. Al fin podemos contar toda la historia:los archivos cambian al estado pedido, pero debemos dejar la branch master. Cualquier commit de aquí en adelante, llevan tus archivos por un nuevo camino, el podrá ser nombrado posteriormente.

En otras palabras, luego de traer un estado viejo, Git automáticamente te pone en una nueva branch sin nombre, la cual puede ser nombrada y salvada con git checkout -b.

Arreglos Rápidos

Estás en medio de algo cuando te piden que dejes todo y soluciones un bug recién descubierto:

$ git commit -a
$ git checkout -b arreglos SHA1_HASH

Luego, una vez que solucionaste el bug:

$ git commit -a -m "Bug arreglado"
$ git push  # al repositorio central
$ git checkout master

y continúa con el trabajo en tu tarea original.

Flujo De Trabajo Ininterrumpido

Algunos proyectos requieren que tu código sea evaluado antes de que puedas subirlo. Para hacer la vida más fácil para aquellos que revisan tu código, si tienes algún cambio grande para hacer, puedes partirlo en dos o mas partes, y hacer que cada parte sea evaluada por separado.

¿Que pasa si la segunda parte no puede ser escrita hasta que la primera sea aprobada y subida? En muchos sistemas de control de versiones, deberías enviar primero el código a los evaluadores, y luego esperar hasta que esté aprobado antes de empezar con la segunda parte.

En realidad, eso no es del todo cierto, pero en estos sistemas, editar la Parte II antes de subir la Parte I involucra sufrimiento e infortunio. En Git, los branches y merges son indoloros (un termino técnico que significa rápidos y locales). Entonces, luego de que hayas hecho commit de la primera parte y la hayas enviado a ser revisada:

$ git checkout -b parte2

Luego, escribe la segunda parte del gran cambio sin esperar a que la primera sea aceptada. Cuando la primera parte sea aprobada y subida,

$ git checkout master
$ git merge parte2
$ git branch -d parte2  # ya no se necesita esta branch

y la segunda parte del cambio está lista para la evaluación.

¡Pero esperen! ¿Qué pasa si no fuera tan simple? Digamos que tuviste un error en la primera parte, el cual hay que corregir antes de subir los cambios. ¡No hay problema! Primero, vuelve a la branch master usando

$ git checkout master

Soluciona el error en la primera parte del cambio y espera que sea aprobado. Si no lo es, simplemente repite este paso. Probablemente quieras hacer un merge de la versión arreglada de la Parte I con la Parte II:

$ git checkout parte2
$ git merge master

Ahora es igual que lo anterior. Una vez que la primera parte sea aprobada:

$ git checkout master
$ git merge parte2
$ git branch -d parte2

y nuevamente, la segunda parte está lista para ser revisada.

Es fácil extender este truco para cualquier cantidad de partes.

Reorganizando Una Mezcla

Quizás quieras trabajar en todos los aspectos de un proyecto sobre la misma branch. Quieres dejar los trabajos-en-progreso para ti y quieres que otros vean tus commits solo cuando han sido pulcramente organizados. Inicia un par de branches:

$ git checkout -b prolijo
$ git checkout -b mezcla

A continuación, trabaja en lo que sea: soluciona bugs, agrega prestaciones, agrega código temporal o lo que quieras, haciendo commits seguidos a medida que avanzas. Entonces:

$ git checkout prolijo
$ git cherry-pick SHA1_HASH

aplica un commit dado a la branch "prolijo". Con cherry-picks apropiados, puedes construir una rama que contenga solo el código permanente, y los commits relacionados juntos en un grupo.

Administrando branches

Lista todas las branches escribiendo:

$ git branch

Siempre hay una branch llamada "master", y es en la que comienzas por defecto. Algunos aconsejan dejar la rama "master" sin tocar y el crear nuevas branches para tus propios cambios.

Las opciones -d y -m te permiten borrar y mover (renombrar) branches. Mira en git help branch

La branch "master" es una convención útil. Otros pueden asumir que tu repositorio tiene una branch con este nombre, y que contiene la versión oficial del proyecto. Puedes renombrar o destruir la branch "master", pero también podrías respetar esta costumbre.

Branches Temporales

Después de un rato puedes notar que estás creando branches de corta vida de manera frecuente por razones similares: cada branch sirve simplemente para salvar el estado actual y permitirte saltar a un estado anterior para solucionar un bug de alta prioridad o algo.

Es análogo a cambiar el canal de la TV temporalmente, para ver que otra cosa están dando. Pero en lugar de apretar un par de botones, tienes que crear, hacer checkout y eliminar branches y commits temporales. Por suerte, Git tiene un aatajo que es tan conveniente como un control remoto de TV:

$ git stash

Esto guarda el estado actual en un lugar temporal (un stash) y restaura el estado anterior. Tu directorio de trabajo se ve idéntico a como estaba antes de que comenzaras a editar, y puedes solucionar bugs, traer cambios desde otros repositorios, etc. Cuando quieras volver a los cambios del stash, escribe:

$ git stash apply  # Puedes necesitar corregir conflictos

Puedes tener varios stashes, y manipularlos de varias maneras. Mira git help stash. Como es de imaginar, Git mantiene branches de manera interna para lograr este truco mágico.

Trabaja como quieras

Aplicaciones como Mozilla Firefox permiten tener varias pestañas y ventanas abiertas. Cambiar de pestaña te da diferente contenido en la misma ventana. Los branches en git son como pestañas para tu directorio de trabajo. Siguiendo esta analogía, el clonar es como abrir una nueva ventana. La posibilidad de ambas cosas es lo que mejora la experiencia del usuario.

En un nivel más alto, varios window managers en Linux soportan múltiples escritorios. Usar branches en Git es similar a cambiar a un escritorio diferente, mientras clonar es similar a conectar otro monitor para ganar un nuevo escritorio.

Otro ejemplo es el programa screen. Esta joya permite crear, destruir e intercambiar entre varias sesiones de terminal sobre la misma terminal. En lugar de abrir terminales nuevas (clone), puedes usar la misma si ejecutas screen (branch). De hecho, puedes hacer mucho más con screen, pero eso es un asunto para otro manual.

Usar clone, branch y merge, es rápido y local en Git, animándote a usar la combinación que más te favorezca. Git te permite trabajar exactamente como prefieras.

Capítulo 5. Lessons of History

A consequence of Git’s distributed nature is that history can be edited easily. But if you tamper with the past, take care: only rewrite that part of history which you alone possess. Just as nations forever argue over who committed what atrocity, if someone else has a clone whose version of history differs to yours, you will have trouble reconciling when your trees interact.

Of course, if you control all the other trees too, then there is no problem since you can overwrite them.

Some developers strongly feel history should be immutable, warts and all. Others feel trees should be made presentable before they are unleashed in public. Git accommodates both viewpoints. Like cloning, branching and merging, rewriting history is simply another power Git gives you. It is up to you to use it wisely.

I Stand Corrected

Did you just commit, but wish you had typed a different message? Then run:

$ git commit --amend

to change the last message. Realized you forgot to add a file? Run git add to add it, and then run the above command.

Want to include a few more edits in that last commit? Then make those edits and run:

$ git commit --amend -a

… And Then Some

Let’s suppose the previous problem is ten times worse. After a lengthy session you’ve made a bunch of commits. But you’re not quite happy with the way they’re organized, and some of those commit messages could use rewording. Then type:

$ git rebase -i HEAD~10

and the last 10 commits will appear in your favourite $EDITOR. A sample excerpt:

pick 5c6eb73 Added repo.or.cz link
pick a311a64 Reordered analogies in "Work How You Want"
pick 100834f Added push target to Makefile

Then:

  • Remove commits by deleting lines.
  • Reorder commits by reordering lines.
  • Replace "pick" with "edit" to mark a commit for amending.
  • Replace "pick" with "squash" to merge a commit with the previous one.

If you marked a commit for editing, then run:

$ git commit --amend

Otherwise, run:

$ git rebase --continue

So commit early and commit often: you can easily tidy up later with rebase.

Local Changes Last

You’re working on an active project. You make some local commits over time, and then you sync with the official tree with a merge. This cycle repeats itself a few times before you’re ready to push to the central tree.

But now the history in your local Git clone is a messy jumble of your changes and the official changes. You’d prefer to see all your changes in one contiguous section, and after all the official changes.

This is a job for git rebase as described above. In many cases you can use the --onto flag and avoid interaction.

Also see git help rebase for detailed examples of this amazing command. You can split commits. You can even rearrange branches of a tree.

Rewriting History

Occasionally, you need the source control equivalent of airbrushing people out of official photos, erasing them from history in a Stalinesque fashion. For example, suppose we intend to release a project, but it involves a file that should be kept private for some reason. Perhaps I left my credit card number in a text file and accidentally added it to the project. Deleting the file is insufficient, for the file can be accessed from older commits. We must remove the file from all commits:

$ git filter-branch --tree-filter 'rm top/secret/file' HEAD

See git help filter-branch, which discusses this example and gives a faster method. In general, filter-branch lets you alter large sections of history with a single command.

Afterwards, the .git/refs/original directory describes the state of affairs before the operation. Check the filter-branch command did what you wanted, then delete this directory if you wish to run more filter-branch commands.

Lastly, replace clones of your project with your revised version if you want to interact with them later.

Making History

Want to migrate a project to Git? If it’s managed with one of the more well-known systems, then chances are someone has already written a script to export the whole history to Git.

Otherwise, look up git fast-import, which reads text input in a specific format to create Git history from scratch. Typically a script using this command is hastily cobbled together and run once, migrating the project in a single shot.

As an example, paste the following listing into temporary file, such as /tmp/history:

commit refs/heads/master
committer Alice <alice@example.com> Thu, 01 Jan 1970 00:00:00 +0000
data <<EOT
Initial commit.
EOT

M 100644 inline hello.c
data <<EOT
#include <stdio.h>

int main() {
  printf("Hello, world!\n");
  return 0;
}
EOT


commit refs/heads/master
committer Bob <bob@example.com> Tue, 14 Mar 2000 01:59:26 -0800
data <<EOT
Replace printf() with write().
EOT

M 100644 inline hello.c
data <<EOT
#include <unistd.h>

int main() {
  write(1, "Hello, world!\n", 14);
  return 0;
}
EOT

Then create a Git repository from this temporary file by typing:

$ mkdir project; cd project; git init
$ git fast-import < /tmp/history

You can checkout the latest version of the project with:

$ git checkout master .

The git fast-export command converts any git repository to the git fast-import format, whose output you can study for writing exporters, and also to transport git repositories in a human-readable format. Indeed, these commands can send repositories of text files over text-only channels.

Where Did It All Go Wrong?

You’ve just discovered a broken feature in your program which you know for sure was working a few months ago. Argh! Where did this bug come from? If only you had been testing the feature as you developed.

It’s too late for that now. However, provided you’ve been committing often, Git can pinpoint the problem:

$ git bisect start
$ git bisect bad SHA1_OF_BAD_VERSION
$ git bisect good SHA1_OF_GOOD_VERSION

Git checks out a state halfway in between. Test the feature, and if it’s still broken:

$ git bisect bad

If not, replace "bad" with "good". Git again transports you to a state halfway between the known good and bad versions, narrowing down the possibilities. After a few iterations, this binary search will lead you to the commit that caused the trouble. Once you’ve finished your investigation, return to your original state by typing:

$ git bisect reset

Instead of testing every change by hand, automate the search by running:

$ git bisect run COMMAND

Git uses the return value of the given command, typically a one-off script, to decide whether a change is good or bad: the command should exit with code 0 when good, 125 when the change should be skipped, and anything else between 1 and 127 if it is bad. A negative return value aborts the bisect.

You can do much more: the help page explains how to visualize bisects, examine or replay the bisect log, and eliminate known innocent changes for a speedier search.

Who Made It All Go Wrong?

Like many other version control systems, Git has a blame command:

$ git blame FILE

which annotates every line in the given file showing who last changed it, and when. Unlike many other version control systems, this operation works offline, reading only from local disk.

Personal Experience

In a centralized version control system, history modification is a difficult operation, and only available to administrators. Cloning, branching, and merging are impossible without network communication. So are basic operations such as browsing history, or committing a change. In some systems, users require network connectivity just to view their own changes or open a file for editing.

Centralized systems preclude working offline, and need more expensive network infrastructure, especially as the number of developers grows. Most importantly, all operations are slower to some degree, usually to the point where users shun advanced commands unless absolutely necessary. In extreme cases this is true of even the most basic commands. When users must run slow commands, productivity suffers because of an interrupted work flow.

I experienced these phenomena first-hand. Git was the first version control system I used. I quickly grew accustomed to it, taking many features for granted. I simply assumed other systems were similar: choosing a version control system ought to be no different from choosing a text editor or web browser.

I was shocked when later forced to use a centralized system. My often flaky internet connection matters little with Git, but makes development unbearable when it needs to be as reliable as local disk. Additionally, I found myself conditioned to avoid certain commands because of the latencies involved, which ultimately prevented me from following my desired work flow.

When I had to run a slow command, the interruption to my train of thought dealt a disproportionate amount of damage. While waiting for server communication to complete, I’d do something else to pass the time, such as check email or write documentation. By the time I returned to the original task, the command had finished long ago, and I would waste more time trying to remember what I was doing. Humans are bad at context switching.

There was also an interesting tragedy-of-the-commons effect: anticipating network congestion, individuals would consume more bandwidth than necessary on various operations in an attempt to reduce future delays. The combined efforts intensified congestion, encouraging individuals to consume even more bandwidth next time to avoid even longer delays.

Capítulo 6. Git Grandmastery

This pretentiously named page is my dumping ground for uncategorized Git tricks.

Source Releases

For my projects, Git tracks exactly the files I’d like to archive and release to users. To create a tarball of the source code, I run:

$ git archive --format=tar --prefix=proj-1.2.3/ HEAD

Changelog Generation

It’s good practice to keep a changelog, and some projects even require it. If you’ve been committing frequently, which you should, generate a Changelog by typing:

$ git log > ChangeLog

Git Over SSH, HTTP

Suppose you have ssh access to a web server, but Git is not installed. Though less efficient than its native protocol, Git can communicate over HTTP.

Download, compile and install Git in your account, and create a repository in your web directory:

$ GIT_DIR=proj.git git init

In the "proj.git" directory, run:

$ git --bare update-server-info
$ chmod a+x hooks/post-update

From your computer, push via ssh:

$ git push web.server:/path/to/proj.git master

and others get your project via:

$ git clone http://web.server/proj.git

Git Over Anything

Want to synchronize repositories without servers, or even a network connection? Need to improvise during an emergency? We’ve seen git fast-export and git fast-import can convert repositories to a single file and back. We could shuttle such files back and forth to transport git repositories over any medium, but a more efficient tool is git bundle.

The sender creates a bundle:

$ git bundle create somefile HEAD

then transports the bundle, somefile, to the other party somehow: email, thumb drive, floppy disk, an xxd printout and an OCR machine, reading bits over the phone, smoke signals, etc. The receiver retrieves commits from the bundle by typing:

$ git pull somefile

The receiver can even do this from an empty repository. Despite its size, somefile contains the entire original git repository.

In larger projects, eliminate waste by bundling only changes the other repository lacks:

$ git bundle create somefile HEAD ^COMMON_SHA1

If done frequently, one could easily forget which commit was last sent. The help page suggests using tags to solve this. Namely, after you send a bundle, type:

$ git tag -f lastbundle HEAD

and create new refresher bundles with:

$ git bundle create newbundle HEAD ^lastbundle

Patches: The Global Currency

Patches are text representations of your changes that can be easily understood by computers and humans alike. This gives them universal appeal. You can email a patch to developers no matter what version control system they’re using. As long as your audience can read their email, they can see your edits. Similarly, on your side, all you require is an email account: there’s no need to setup an online Git repository.

Recall from the first chapter:

$ git diff COMMIT

outputs a patch which can be pasted into an email for discussion. In a Git repository, type:

$ git apply < FILE

to apply the patch.

In more formal settings, when author names and perhaps signatures should be recorded, generate the corresponding patches past a certain point by typing:

$ git format-patch START_COMMIT

The resulting files can be given to git-send-email, or sent by hand. You can also specify a range of commits:

$ git format-patch START_COMMIT..END_COMMIT

On the receving end, save an email to a file, then type:

$ git am < FILE

This applies the incoming patch and also creates a commit, including information such as the author.

With a browser email client, you may need to click a button to see the email in its raw original form before saving the patch to a file.

There are slight differences for mbox-based email clients, but if you use one of these, you’re probably the sort of person who can figure them out easily without reading tutorials!

Sorry, We’ve Moved

From earlier chapters, we’ve seen that after cloning a repository, running git push or git pull will automatically push to or pull from the original URL. How does Git do this? The secret lies in config options initialized created with the clone. Let’s take a peek:

$ git config --list

The remote.origin.url option controls the source URL; "origin" is a nickname given to the source repository. As with the "master" branch convention, we may change or delete this nickname but there is usually no reason for doing so.

If the the original repository moves, we can update the URL via:

$ git config remote.origin.url NEW_URL

The branch.master.merge option specifies the default remote branch in a git pull. During the initial clone, it is set to the current branch of the source repository, so even if the HEAD of the source repository subsequently moves to a different branch, a later pull will faithfully follow the original branch.

This option only applies to the repository we first cloned from, which is recorded in the option branch.master.remote. If we pull in from other repositories we must explicitly state which branch we want:

$ git pull ANOTHER_URL master

The above explains why some of our earlier push and pull examples occasionally required arguments.

Remote Branches

When you clone a repository, you also clone all its branches. You may not have noticed this because Git hides them away: you must ask for them specifically. This prevents branches in the remote repository from interfering with your branches, and also makes Git easier for beginners.

List the remote branches with:

$ git branch -r

You should see something like:

origin/HEAD
origin/master
origin/experimental

These represent branches and the HEAD of the remote repository, and can be used in regular Git commands. For example, suppose you have made many commits, and wish to compare against the last fetched version. You could search through the logs for the appropriate SHA1 hash, but it’s much easier to type:

$ git diff origin/HEAD

Or you can see what the "experimental" branch has been up to:

$ git log origin/experimental

Multiple Remotes

Suppose two other developers are working on our project, and we want to keep tabs on both. We can follow more than one repository at a time with:

$ git remote add other ANOTHER_URL
$ git pull other some_branch

Now we have merged in a branch from the second repository, and we have easy access to all branches of all repositories:

$ git diff origin/experimental^ other/some_branch~5

But what if we just want to compare their changes without affecting our own work? In other words, we want to examine their branches without having their changes invade our working directory. In this case, rather than pull, run:

$ git fetch        # Fetch from origin, the default.
$ git fetch other  # Fetch from the second programmer.

This fetches their histories and nothing more, so although the working directory remains untouched, we can refer to any branch of any repository in a Git command. By the way, behind the scenes, a pull is simply a fetch followed by git merge; recall the latter merges a given commit into the working directory. Usually we pull because we want to merge after a fetch; this situation is a notable exception.

See git help remote for how to remove remote repositories, ignore certain branches, and more.

Commit What Changed

Telling Git when you’ve added, deleted and renamed files is troublesome for certain projects. Instead, you can type:

$ git add .
$ git add -u

Git will look at the files in the current directory and work out the details by itself. Instead of the second add command, run git commit -a if you also intend to commit at this time.

You can perform the above in a single pass with:

$ git ls-files -d -m -o -z | xargs -0 git update-index --add --remove

The -z and -0 options prevent ill side-effects from filenames containing strange characters. Note this command adds ignored files. You may want to use the -x or -X option.

My Commit Is Too Big!

Have you neglected to commit for too long? Been coding furiously and forgotten about source control until now? Made a series of unrelated changes, because that’s your style?

No worries. Run:

$ git add -p

For each edit you made, Git will show you the hunk of code that was changed, and ask if it should be part of the next commit. Answer with "y" or "n". You have other options, such as postponing the decision; type "?" to learn more.

Once you’re satisfied, type

$ git commit

to commit precisely the changes you selected (the staged changes). Make sure you omit the -a option, otherwise Git will commit all the edits.

What if you’ve edited many files in many places? Reviewing each change one by one becomes frustratingly mind-numbing. In this case, use git add -i, whose interface is less straightforward, but more flexible. With a few keystrokes, you can stage or unstage several files at a time, or review and select changes in particular files only. Alternatively, run git commit --interactive which automatically commits after you’re done.

Staged Changes

So far we have avoided Git’s famous index, but we must now confront it to explain the above. The index is a temporary staging area. Git seldom shuttles data directly between your project and its history. Rather, Git first writes data to the index, and then copies the data in the index to its final destination.

For example, commit -a is really a two-step process. The first step places a snapshot of the current state of every tracked file into the index. The second step permanently records the snapshot now in the index. Committing without the -a option only performs the second step, and only makes sense after running commands that somehow change the index, such as git add.

Usually we can ignore the index and pretend we are reading straight from and writing straight to the history. On this occasion, we want finer control on what gets written to history, and are forced to manipulate the index. We place a snapshot of some, but not all, of our changes into the index, and then permanently record this carefully rigged snapshot.

Don’t Lose Your HEAD

The HEAD tag is like a cursor that normally points at the latest commit, advancing with each new commit. Some Git commands let you move it. For example:

$ git reset HEAD~3

will move the HEAD three commits back. Thus all Git commands now act as if you hadn’t made those last three commits, while your files remain in the present. See the help page for some applications.

But how can you go back to the future? The past commits know nothing of the future.

If you have the SHA1 of the original HEAD then:

$ git reset SHA1

But suppose you never took it down? Don’t worry, for commands like these, Git saves the original HEAD as a tag called ORIG_HEAD, and you can return safe and sound with:

$ git reset ORIG_HEAD

HEAD-hunting

Perhaps ORIG_HEAD isn’t enough. Perhaps you’ve just realized you made a monumental mistake and you need to go back to an ancient commit in a long-forgotten branch.

By default, Git keeps a commit for at least two weeks, even if you ordered Git to destroy the branch containing it. The trouble is finding the appropriate hash. You could look at all the hash values in .git/objects and use trial and error to find the one you want. But there’s a much easier way.

Git records every hash of a commit it computes in .git/logs. The subdirectory refs contains the history of all activity on all branches, while the file HEAD shows every hash value it has ever taken. The latter can be used to find hashes of commits on branches that have been accidentally lopped off.

The reflog command provides a friendly interface to these log files. Try

$ git reflog

Instead of cutting and pasting hashes from the reflog, try:

$ git checkout "@{10 minutes ago}"

Or checkout the 5th-last visited commit via:

$ git checkout "@{5}"

See the “Specifying Revisions” section of git help rev-parse for more.

You may wish to configure a longer grace period for doomed commits. For example:

$ git config gc.pruneexpire "30 days"

means a deleted commit will only be permanently lost once 30 days have passed and git gc is run.

You may also wish to disable automatic invocations of git gc:

$ git conifg gc.auto 0

in which case commits will only be deleted when you run git gc manually.

Building On Git

In true UNIX fashion, Git’s design allows it to be easily used as a low-level component of other programs, such as GUI and web interfaces, alternative command-line interfaces, patch managements tools, importing and conversion tools and so on. In fact, some Git commands are themselves scripts standing on the shoulders of giants. With a little tinkering, you can customize Git to suit your preferences.

One easy trick is to use built-in Git aliases to shorten your most frequently used commands:

$ git config --global alias.co checkout
$ git config --global --get-regexp alias  # display current aliases
alias.co checkout
$ git co foo                              # same as 'git checkout foo'

Another is to print the current branch in the prompt, or window title. Invoking

$ git symbolic-ref HEAD

shows the current branch name. In practice, you most likely want to remove the "refs/heads/" and ignore errors:

$ git symbolic-ref HEAD 2> /dev/null | cut -b 12-

The contrib subdirectory is a treasure trove of tools built on Git. In time, some of them may be promoted to official commands. On Debian and Ubuntu, this directory lives at /usr/share/doc/git-core/contrib.

One popular resident is workdir/git-new-workdir. Via clever symlinking, this script creates a new working directory whose history is shared with the original respository:

$ git-new-workdir an/existing/repo new/directory

The new directory and files within can be thought of as a clone, except since the history is shared, the two trees automatically stay in sync. There’s no need to merge, push or pull.

Daring Stunts

These days, Git makes it difficult for the user to accidentally destroy data. But if you know what you are doing, you can override safeguards for common commands.

Checkout: Uncommitted changes cause checkout to fail. To destroy your changes, and checkout a given commit anyway, use the force flag:

$ git checkout -f COMMIT

On the other hand, if you specify particular paths for checkout, then there are no safety checks. The supplied paths are quietly overwritten. Take care if you use checkout in this manner.

Reset: Reset also fails in the presence of uncommitted changes. To force it through, run:

$ git reset --hard [COMMIT]

Branch: Deleting branches fails if this causes changes to be lost. To force a deletion, type:

$ git branch -D BRANCH  # instead of -d

Similarly, attempting to overwrite a branch via a move fails if data loss would ensue. To force a branch move, type:

$ git branch -M [SOURCE] TARGET  # instead of -m

Unlike checkout and reset, these two commands defer data destruction. The changes are still stored in the .git subdirectory, and can be retrieved by recovering the appropriate hash from .git/logs (see "HEAD-hunting" above). By default, they will be kept for at least two weeks.

Clean: Some git commands refuse to proceed because they’re worried about clobbering untracked files. If you’re certain that all untracked files and directories are expendable, then delete them mercilessly with:

$ git clean -f -d

Next time, that pesky command will work!

Capítulo 7. Secrets Revealed

We take a peek under the hood and explain how Git performs its miracles. I will skimp over details. For in-depth descriptions refer to the user manual.

Invisibility

How can Git be so unobtrusive? Aside from occasional commits and merges, you can work as if you were unaware that version control exists. That is, until you need it, and that’s when you’re glad Git was watching over you the whole time.

Other version control systems don’t let you forget about them. Permissions of files may be read-only unless you explicitly tell the server which files you intend to edit. The central server might be keeping track of who’s checked out which code, and when. When the network goes down, you’ll soon suffer. Developers constantly struggle with virtual red tape and bureaucracy.

The secret is the .git directory in your working directory. Git keeps the history of your project here. The initial "." stops it showing up in ls listings. Except when you’re pushing and pulling changes, all version control operations operate within this directory.

You have total control over the fate of your files because Git doesn’t care what you do to them. Git can easily recreate a saved state from .git at any time.

Integrity

Most people associate cryptography with keeping information secret, but another equally important goal is keeping information safe. Proper use of cryptographic hash functions can prevent accidental or malicious data corruption.

A SHA1 hash can be thought of as a unique 160-bit ID number for every string of bytes you’ll encounter in your life. Actually more than that: every string of bytes that any human will ever use over many lifetimes.

As a SHA1 hash is itself a string of bytes, we can hash strings of bytes containing other hashes. This simple observation is surprisingly useful: look up hash chains. We’ll later see how Git uses it to efficiently guarantee data integrity.

Briefly, Git keeps your data in the ".git/objects" subdirectory, where instead of normal filenames, you’ll find only IDs. By using IDs as filenames, as well as a few lockfiles and timestamping tricks, Git transforms any humble filesystem into an efficient and robust database.

Intelligence

How does Git know you renamed a file, even though you never mentioned the fact explicitly? Sure, you may have run git mv, but that is exactly the same as a git rm followed by a git add.

Git heuristically ferrets out renames and copies between successive versions. In fact, it can detect chunks of code being moved or copied around between files! Though it cannot cover all cases, it does a decent job, and this feature is always improving. If it fails to work for you, try options enabling more expensive copy detection, and consider upgrading.

Indexing

For every tracked file, Git records information such as its size, creation time and last modification time in a file known as the index. To determine whether a file has changed, Git compares its current stats with that held the index. If they match, then Git can skip reading the file again.

Since stat calls are considerably faster than file reads, if you only edit a few files, Git can update its state in almost no time.

Bare Repositories

You may have been wondering what format those online Git repositories use. They’re plain Git repositories, just like your .git directory, except they’ve got names like proj.git, and they have no working directory associated with them.

Most Git commands expect the Git index to live in .git, and will fail on these bare repositories. Fix this by setting the GIT_DIR environment variable to the path of the bare repository, or running Git within the directory itself with the --bare option.

Git’s Origins

This Linux Kernel Mailing List post describes the chain of events that led to Git. The entire thread is a fascinating archaeological site for Git historians.

The Object Database

Here’s how to write a Git-like system from scratch in a few hours.

Blobs

First, a magic trick. Pick a filename, any filename. In an empty directory:

$ echo sweet > YOUR_FILENAME
$ git init
$ git add .
$ find .git/objects -type f

You’ll see .git/objects/aa/823728ea7d592acc69b36875a482cdf3fd5c8d.

How do I know this without knowing the filename? It’s because the SHA1 hash of:

"blob" SP "6" NUL "sweet" LF

is aa823728ea7d592acc69b36875a482cdf3fd5c8d, where SP is a space, NUL is a zero byte and LF is a linefeed. You can verify this by typing:

$ printf "blob 6\000sweet\n" | sha1sum

Git is content-addressable: files are not stored according to their filename, but rather by the hash of the data they contain, in a file we call a blob object. We can think of the hash as a unique ID for a file’s contents, so in a sense we are addressing files by their content. The initial "blob 6" is merely a header consisting of the object type and its length in bytes; it simplifies internal bookkeeping.

Thus I could easily predict what you would see. The file’s name is irrelevant: only the data inside is used to construct the blob object.

You may be wondering what happens to identical files. Try adding copies of your file, with any filenames whatsoever. The contents of .git/objects stay the same no matter how many you add. Git only stores the data once.

By the way, the files within .git/objects are compressed with zlib so you should not stare at them directly. Filter them through zpipe -d, or type:

$ git cat-file -p aa823728ea7d592acc69b36875a482cdf3fd5c8d

which pretty-prints the given object.

Trees

But where are the filenames? They must be stored somewhere at some stage. Git gets around to the filenames during a commit:

$ git commit  # Type some message.
$ find .git/objects -type f

You should now see 3 objects. This time I cannot tell you what the 2 new files are, as it partly depends on the filename you picked. We’ll proceed assuming you chose "rose". If you didn’t, you can rewrite history to make it look like you did:

$ git filter-branch --tree-filter 'mv YOUR_FILENAME rose'
$ find .git/objects -type f

Now you should see the file .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9, because this is the SHA1 hash of its contents:

"tree" SP "32" NUL "100644 rose" NUL 0xaa823728ea7d592acc69b36875a482cdf3fd5c8d

Check this file does indeed contain the above by typing:

$ echo 05b217bb859794d08bb9e4f7f04cbda4b207fbe9 | git cat-file --batch

With zpipe, it’s easy to verify the hash:

$ zpipe -d < .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9 | sha1sum

Hash verification is trickier via cat-file because its output contains more than the raw uncompressed object file.

This file is a tree object: a list of tuples consisting of a file type, a filename, and a hash. In our example, the file type is "100644", which means "rose" is a normal file, and the hash is the blob object that contains the contents of "rose". Other possible file types are executables, symlinks or directories. In the last case, the hash points to a tree object.

If you ran filter-branch, you’ll have old objects you no longer need. Although they will be jettisoned automatically once the grace period expires, we’ll delete them now to make our toy example easier to follow:

$ rm -r .git/refs/original
$ git reflog expire --expire=now --all
$ git prune

For real projects you should typically avoid commands like this, as you are destroying backups. If you want a clean repository, it is usually best to make a fresh clone. Also, take care when directly manipulating .git: what if a Git command is running at the same time, or a sudden power outage occurs? In general, refs should be deleted with git update-ref -d, though usually it’s safe to remove refs/original by hand.

Commits

We’ve explained 2 of the 3 objects. The third is a commit object. Its contents depend on the commit message as well as the date and time it was created. To match what we have here, we’ll have to tweak it a little:

$ git commit --amend -m Shakespeare  # Change the commit message.
$ git filter-branch --env-filter 'export
    GIT_AUTHOR_DATE="Fri 13 Feb 2009 15:31:30 -0800"
    GIT_AUTHOR_NAME="Alice"
    GIT_AUTHOR_EMAIL="alice@example.com"
    GIT_COMMITTER_DATE="Fri, 13 Feb 2009 15:31:30 -0800"
    GIT_COMMITTER_NAME="Bob"
    GIT_COMMITTER_EMAIL="bob@example.com"'  # Rig timestamps and authors.
$ find .git/objects -type f

You should now see .git/objects/49/993fe130c4b3bf24857a15d7969c396b7bc187 which is the SHA1 hash of its contents:

"commit 158" NUL
"tree 05b217bb859794d08bb9e4f7f04cbda4b207fbe9" LF
"author Alice <alice@example.com> 1234567890 -0800" LF
"committer Bob <bob@example.com> 1234567890 -0800" LF
LF
"Shakespeare" LF

As before, you can run zpipe or cat-file to see for yourself.

This is the first commit, so there are no parent commits, but later commits will always contain at least one line identifying a parent commit.

Indistinguishable From Magic

There’s little else to say. We have just exposed the secret behind Git’s powers. It seems too simple: it looks like you could mix together a few shell scripts and add a dash of C code to cook up the above in a matter of hours. In fact, this accurately describes the earliest versions of Git. Nonetheless, apart from ingenious packing tricks to save space, and ingenious indexing tricks to save time, we now know how Git deftly changes a filesystem into a database perfect for version control.

For example, if any file within the object database is corrupted by a disk error, then its hash will no longer match, alerting us to the problem. By hashing hashes of other objects, we maintain integrity at all levels. Commits are atomic, that is, a commit can never only partially record changes: we can only compute the hash of a commit and store it in the database after we already have stored all relevant trees, blobs and parent commits. The object database is immune to unexpected interruptions such as power outages.

We defeat even the most devious adversaries. Suppose somebody attempts to stealthily modify the contents of a file in an ancient version of a project. To keep the object database looking healthy, they must also change the hash of the corresponding blob object since it’s now a different string of bytes. This means they’ll have to change the hash of any tree object referencing the file, and in turn change the hash of all commit objects involving such a tree, in addition to the hashes of all the descendants of these commits. This implies the hash of the official head differs to that of the bad repository. By following the trail of mismatching hashes we can pinpoint the mutilated file, as well as the commit where it was first corrupted.

In short, so long as the 20 bytes representing the last commit are safe, it’s impossible to tamper with a Git repository.

What about Git’s famous features? Branching? Merging? Tags? Mere details. The current head is kept in the file .git/HEAD, which contains a hash of a commit object. The hash gets updated during a commit as well as many other commands. Branches are almost the same: they are files in .git/refs/heads. Tags too: they live in .git/refs/tags but they are updated by a different set of commands.

Capítulo 8. Git Shortcomings

There are some Git issues I’ve swept under the carpet. Some can be handled easily with scripts and hooks, some require reorganizing or redefining the project, and for the few remaining annoyances, one will just have to wait. Or better yet, pitch in and help!

SHA1 Weaknesses

As time passes, cryptographers discover more and more SHA1 weaknesses. Already, finding hash collisions is feasible for well-funded organizations. Within years, perhaps even a typical PC will have enough computing power to silently corrupt a Git repository.

Hopefully Git will migrate to a better hash function before further research destroys SHA1.

Microsoft Windows

Git on Microsoft Windows can be cumbersome:

Unrelated Files

If your project is very large and contains many unrelated files that are constantly being changed, Git may be disadvantaged more than other systems because single files are not tracked. Git tracks changes to the whole project, which is usually beneficial.

A solution is to break up your project into pieces, each consisting of related files. Use git submodule if you still want to keep everything in a single repository.

Who’s Editing What?

Some version control systems force you to explicitly mark a file in some way before editing. While this is especially annoying when this involves talking to a central server, it does have two benefits:

  1. Diffs are quick because only the marked files need be examined.
  2. One can discover who else is working on the file by asking the central server who has marked it for editing.

With appropriate scripting, you can achieve the same with Git. This requires cooperation from the programmer, who should execute particular scripts when editing a file.

File History

Since Git records project-wide changes, reconstructing the history of a single file requires more work than in version control systems that track individual files.

The penalty is typically slight, and well worth having as other operations are incredibly efficient. For example, git checkout is faster than cp -a, and project-wide deltas compress better than collections of file-based deltas.

Initial Clone

Creating a clone is more expensive than checking out code in other version control systems when there is a lengthy history.

The initial cost is worth paying in the long run, as most future operations will then be fast and offline. However, in some situations, it may be preferable to create a shallow clone with the --depth option. This is much faster, but the resulting clone has reduced functionality.

Volatile Projects

Git was written to be fast with respect to the size of the changes. Humans make small edits from version to version. A one-liner bugfix here, a new feature there, emended comments, and so forth. But if your files are radically different in successive revisions, then on each commit, your history necessarily grows by the size of your whole project.

There is nothing any version control system can do about this, but standard Git users will suffer more since normally histories are cloned.

The reasons why the changes are so great should be examined. Perhaps file formats should be changed. Minor edits should only cause minor changes to at most a few files.

Or perhaps a database or backup/archival solution is what is actually being sought, not a version control system. For example, version control may be ill-suited for managing photos periodically taken from a webcam.

If the files really must be constantly morphing and they really must be versioned, a possibility is to use Git in a centralized fashion. One can create shallow clones, which checks out little or no history of the project. Of course, many Git tools will be unavailable, and fixes must be submitted as patches. This is probably fine as it’s unclear why anyone would want the history of wildly unstable files.

Another example is a project depending on firmware, which takes the form of a huge binary file. The history of the firmware is uninteresting to users, and updates compress poorly, so firmware revisions would unnecessarily blow up the size of the repository.

In this case, the source code should be stored in a Git repository, and the binary file should be kept separately. To make life easier, one could distribute a script that uses Git to clone the code, and rsync or a Git shallow clone for the firmware.

Global Counter

Some centralized version control systems maintain a positive integer that increases when a new commit is accepted. Git refers to changes by their hash, which is better in many circumstances.

But some people like having this integer around. Luckily, it’s easy to write scripts so that with every update, the central Git repository increments an integer, perhaps in a tag, and associates it with the hash of the latest commit.

Every clone could maintain such a counter, but this would probably be useless, since only the central repository and its counter matters to everyone.

Empty Subdirectories

Empty subdirectories cannot be tracked. Create dummy files to work around this problem.

The current implementation of Git, rather than its design, is to blame for this drawback. With luck, once Git gains more traction, more users will clamour for this feature and it will be implemented.

Initial Commit

A stereotypical computer scientist counts from 0, rather than 1. Unfortunately, with respect to commits, git does not adhere to this convention. Many commands are unfriendly before the initial commit. Additionally, some corner cases must be handled specially, such as rebasing a branch with a different initial commit.

Git would benefit from defining the zero commit: as soon as a repository is constructed, HEAD would be set to the string consisting of 20 zero bytes. This special commit represents an empty tree, with no parent, at some time predating all Git repositories.

Then running git log, for example, would inform the user that no commits have been made yet, instead of exiting with a fatal error. Similarly for other tools.

Every initial commit is implicitly a descendant of this zero commit. For example, rebasing an unrelated branch would cause the whole branch to be grafted on to the target. Currently, all but the initial commit is applied, resulting in a merge conflict. One workaround is to use git checkout followed by git commit -C on the initial commit, then rebase the rest.

There are worse cases unfortunately. If several branches with different initial commits are merged together, then rebasing the result requires substantial manual intervention.

Apéndice A. Translating This Guide

Clone the source, then create a directory corresponding to the target language’s IETF tag: see the W3C article on internationalization. For example, English is "en", Japanese is "ja", and Traditional Chinese is "zh-Hant". In the new directory, and translate the txt files from the "en" subdirectory.

For instance, to translate the guide into Klingon, you might type:

$ git clone git://repo.or.cz/gitmagic.git
$ cd gitmagic
$ mkdir tlh  # "tlh" is the IETF language code for Klingon.
$ cd tlh
$ cp ../en/intro.txt .
$ edit intro.txt  # Translate the file.

and so on for each text file. You can review your work incrementally:

$ make LANG=tlh
$ firefox book.html

Commit your changes often, then let me know when they’re ready. GitHub.com has an interface that facilitates this: fork the "gitmagic" project, push your changes, then ask me to merge.

I like to have translations follow the above scheme so my scripts can produce HTML and PDF versions. Also, it conveniently keeps all the translations in the official repository. But please do whatever suits you best: for example, the Chinese translators used Google Docs. I’m happy as long as your work enables more people to access my work.