Tabla de contenidos
Tabla de contenidos
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
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.
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.
Tabla de contenidos
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.
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.
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.
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 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.
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.
Tabla de contenidos
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.
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"
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
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 estoPuedes 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
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.
Si ya descargaste una copia de un proyecto usando git clone, puedes actualizarte a la última versión con:
$ git pull
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.
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.
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:
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
Como en A tenemos los archivos guardados, podemos recuperarlos :
$ git checkout A ARCHIVOS...
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.
Tabla de contenidos
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.
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.
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
¿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
¿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.
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
¿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.
Tabla de contenidos
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.
¿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.
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.
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.
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.
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.
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.
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.
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.
Tabla de contenidos
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.
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
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:
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.
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.
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.
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.
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.
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.
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.
Tabla de contenidos
This pretentiously named page is my dumping ground for uncategorized Git tricks.
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
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
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
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 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!
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.
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
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.
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.
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.
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.
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
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.
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.
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!
Tabla de contenidos
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.
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.
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.
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.
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.
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.
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.
Here’s how to write a Git-like system from scratch in a few hours.
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.
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.
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.
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.
Tabla de contenidos
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!
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.
Git on Microsoft Windows can be cumbersome:
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.
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:
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.
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.
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.
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.
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 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.
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.
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.