========================= Rápida introducción a Git ========================= ------------------------------------------------ Cómo empezar a usar Git y no morir en el intento ------------------------------------------------ :Autor: Fernando J. Pereda :Licencia: http://creativecommons.org/licenses/by-nc-sa/2.5/es/ .. contents:: **Tabla de contenidos** :backlinks: none :depth: 1 Introducción a Git ================== ¿ Git ? ======= Git es un sistema de control de versiones distribuido. Es decir, nos ayudará a mantener ficheros en directorios. Es parecido en cierto modo a los archiconocidos CVS y SVN (Subversion) pero por suerte es distinto. Es más parecido a darcs, mercurial, bzr y demás sistemas de versiones distribuidos. ¿Distribuidos? Si. No existe el concepto de cliente y servidor, si no de repositorios. Cada repositorio tiene toda la historia del proyecto o del conjunto de ficheros que queremos controlar. Así que de primeras, es normal que una copia del repositorio ocupe más. Sin embargo esto tiene sus ventajas: * Cualquier tipo de consulta es mucho más rápida dado que solo depende de la velocidad del equipo local, no depende de servidores externos. * No es necesario estar conectado constantemente para trabajar sobre los ficheros. * Es muy fácil que varias personas trabajen sobre los mismos ficheros ya que no hay que dar acceso de escritura ni hacer cuentas en ningún servidor. Git está pensado para trabajar de esta forma, distribuido. Primeros pasos ============== Empezaremos por crear un repositorio y trabajaremos sobre varios ficheros. Más adelante veremos cómo varias personas podrían trabajar sobre esos ficheros sin muchos problemas. Para crear un repositorio haremos lo siguiente: :: $ mkdir git-intro $ cd git-intro $ git init-db defaulting to local storage area Con esto Git ha creado toda una estructura de directorios donde guardará su información interna, normalmente no tocaremos esto a mano, pero es importante saber qué es cada cosa: :: $ tree .git/ .git/ |-- HEAD |-- config |-- description |-- hooks | |-- applypatch-msg | |-- commit-msg | |-- post-commit | |-- post-update | |-- pre-applypatch | |-- pre-commit | `-- update |-- info | `-- exclude |-- objects | |-- info | `-- pack `-- refs |-- heads `-- tags 8 directories, 11 files Brevemente: (todo tendrá más sentido en un rato) HEAD Contiene la rama actual sobre la que estamos trabajando. config Configuración del repositorio. El formato es tipo INI. description Descripción del repositorio, usado por gitweb. hooks/ Contiene diferentes scripts y filtros que se ejecutarán en distintas fases de Git. Están todos desactivados por defecto, para activarlos hay que darles permisos de ejecución. Los hooks por defecto son bastante útiles en muchos casos y además contienen documentación para construir hooks que nosotros podamos necesitar. info/ Otros ficheros. objects/ Contiene los objetos de Git. Un objeto puede ser de varios tipos (commit, blob, merge, ...) y es inmutable, una vez creado no se puede modificar. objects/packs/ Los packs son objetos especiales, como su nombre indica son paquetes con varios objetos, útiles para ahorrar espacio y por razones de rendimiento. refs/heads/ Las ramas que creemos se crearán aquí, realmente una rama no es más que un hash SHA1 indicando dónde está el commit que inicia la rama. refs/tags/ Aquí se guardan las etiquetas, una etiqueta marca un punto en la historia del proyecto, útil para marcar las versiones o momentos especiales del desarrollo. Una etiqueta es igual que una rama solo que la etiqueta no cambia, marca un momento exacto. Ahora añadiremos un fichero chorra y haremos el primer commit en Git. Para eso tenemos que hacer una pequeña configuración en nuestro repositorio. Tenemos que poner nuestro nombre y nuestro email: :: $ git repo-config user.name "Ego mi mei" $ git repo-config user.email emm@example.com Esto es necesario para que nuestros cambios queden bien archivados y sean fáciles de compartir, hay que recordar que parte de la idea de usar un sistema distribuido ¡es compartir nuestros cambios! :: $ cp /piribi/piribi/OSL-2.1 COPYING $ git add COPYING ;# Con esto le decimos a Git que controle COPYING $ git commit -s -m "Initial commit" -a Committing initial tree 7e86d6c3d9d46e7741ca30dc39328661b22bf119 Las opciones de ``commit`` que hemos utilizado son: ``-s`` Firmar el commit. Esto añade una línea 'Signed-off-by:' al mensaje, el significado de esta línea puede variar de proyecto a proyecto, pero básicamente significará que tu tienes la propiedad intelectual sobre el parche, o que tienes permiso para enviarlo (ya sea implícito por la licencia o explícito por parte del autor original). Muchos proyectos no aceptan parches y cambios que no lleven esta línea. ``-m`` Para indicarle un mensaje en el mismo comando, de no ser así Git abrirá nuestro ``EDITOR``. ``-a`` Le decimos que envíe todos los ficheros que hayan cambiado desde el último commit. El hecho de que el mensaje esté en inglés no es una coincidencia. Es de buena educación hacerlo así si pretendemos compartir nuestro trabajo con otros. Al comando anterior Git responderá con algo *similar* a lo que me contestó a mi. Todo esto ocurre en nuestra máquina, en local, sin necesidad de acceder a la red. Incluso si el repositorio es una copia (``clone``) de uno remoto, el ``commit`` es siempre local. ¿ Y ya está ? ============= Empezaremos por añadir más ficheros: :: $ git add main.c $ git commit -s -m "My new shiny main." -a Por defecto trabajamos en la rama ``master`` para obtener el parche correspondiente a nuestro último commit podemos hacerlo de varias formas, lo más simple es hacer: :: $ git diff HEAD^ diff --git a/main.c b/main.c new file mode 100644 index 0000000..6002919 --- /dev/null +++ b/main.c @@ -0,0 +1,13 @@ +#include + +int main(int argc, char *argv[]) +{ + int n; + + n = atoi(argv[1]); + + while (n-- > 0) + puts("Cuack!"); + + return 0; +} Donde ``HEAD^`` significa 'el primer antecesor de la rama actual'. ``HEAD`` siempre se refiere a la rama actual, en este caso será ``master`` ya que no la hemos cambiado. Así que lo que le estamos pidiendo a Git es la diferencia entre el estado actual y el estado anterior. Además podemos conseguir un log de todos los cambios del proyecto usando ``log``: :: $ git log commit f7faea740d7841e58eb349fdfbfe287ed82c6382 Author: Fernando J. Pereda Date: Wed Apr 12 19:56:58 2006 +0200 My new shiny main. Signed-off-by: Ego mi mei commit 69345fadb85fcdbed6082071c3bfc918bc930ee1 Author: Fernando J. Pereda Date: Wed Apr 12 18:55:49 2006 +0200 Initial commit. Signed-off-by: Ego mi mei Y podemos incluir un resumen de todos los cambios usando una utilidad muy maja llamada ``shortlog``: :: $ git log | git shortlog Fernando J. Pereda: Initial commit. My new shiny main. No te vayas por las ramas ========================= Antes he mencionado las ramas, en Git trabajamos con ramas. Las ramas son distintos caminos del desarrollo; las ramas en Git son *baratas* así que crearemos muchas ramas a lo largo de la vida del proyecto. Una rama no es más que una referencia al commit que hace de *cima* de esa rama. Para crear una rama utilizamos el comando ``checkout``, crearemos una rama llamada (sorpresa sorpresa) *mirama*: :: $ git checkout -b mirama Ahora hemos creado una nueva rama llamada *mirama* y hemos cambiado a ella, para ver la rama en la que estamos usaremos el comando ``branch``: :: $ git branch master * mirama Como *mirama* es un nombre malísimo, la borraremos y le daremos un nombre mejor: :: $ git branch -D mirama Cannot delete the branch you are on. Oops, no podemos borrar la rama en la que estamos, volvamos a ``master`` y la borraremos desde allí: :: $ git checkout master $ git branch -D mirama Deleted branch mirama. $ git checkout -b makeify Ahora podemos trabajar en esta rama, en la que lo que haremos será añadir un Makefile poco a poco: :: $ vim Makefile $ git add Makefile $ git commit -s -m "Add a simple Makefile." -a En un momento dado de la vida de esta rama podemos querer un parche entre el estado actual de la rama y donde dejamos master, podemos usar, una vez más, el comando ``diff``: :: $ git diff master diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d3a108a --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +SRCS=main.c + +OBJS=$(SRCS:.c=.o) + +ducky: $(OBJS) + $(CC) -o ducky $(OBJS) Mmmm, revisando ``main.c`` nos damos cuenta de que tenemos un problema, hay que arreglarlo. Cambiaremos a esa rama, lo arreglaremos y podremos seguir trabajando en la rama que necesitemos: :: $ git checkout master $ vim main.c $ git diff diff --git a/main.c b/main.c index 6002919..3a7599d 100644 --- a/main.c +++ b/main.c @@ -4,6 +4,11 @@ int main(int argc, char *argv[]) { int n; + if (argc < 2) { + fprintf(stderr,"%s: Necesito un número!\n",argv[0]); + return 1; + } + n = atoi(argv[1]); while (n-- > 0) Cuando estamos contentos con nuestro trabajo, podemos incluirlo en la rama: :: $ git commit -s -m "Fix a potential segfault when no args are provided" -a Con este commit, la situación actual de nuestro repositorio es la siguiente: :: o---o---o "master" \ \ x "makeify" Hemos creado en ``master`` un cambio que no hay en ``makeify``. Seguiremos trabajando en ``makeify`` para mejorar un poco el Makefile y luego veremos cómo incluir los cambios de una rama, en la otra. :: $ git checkout makeify $ vim Makefile $ git diff diff --git a/Makefile b/Makefile index d3a108a..b43f232 100644 --- a/Makefile +++ b/Makefile @@ -4,3 +4,6 @@ OBJS=$(SRCS:.c=.o) ducky: $(OBJS) $(CC) -o ducky $(OBJS) + +clean: + rm *.o ducky $ git commit -s -m "Add a clean target." -a Nuestra situación actual sería: :: o---o---o "master" \ \ x---x "makeify" Ahora tenemos dos commits en ``makeify`` que no hay en ``master`` y creemos que es hora de ir incluyendo los cambios. Aquí tenemos dos opciones, un ``merge`` de ``makeify`` en ``master`` o hacer un ``rebase`` de ``makeify`` sobre ``master`` y luego un ``merge``. Lo mejor es lo segundo, aunque es importante explicar las diferencias. En el primer caso, haríamos: :: $ git checkout master $ git pull . makeify [ ... información del merge ... ] Y la situación sería: :: o---o---o---@ "master" \ | \ / x---x "makeify" Ese commit marcado con **@** es un commit especial, es un ``merge``. Realmente no hay grandes problemas con esto, el único problema es que la historia no queda del todo limpia, lo mejor es hacer el ``rebase``: :: $ git checkout makeify $ git rebase master [ ... información del rebase ... ] Y nos dejaría la siguiente situación: :: o---o---o "master" \ \ x---x "makeify" Ahora no hay commits en ``master`` que no existan en ``makeify`` así que si ahora hacemos un ``merge``, realmente haremos un ``merge`` especial, en la jerga de Git se llama ``fast-forward``: :: $ git checkout master $ git pull . makeify Updating from 13b0a5cdc355012d8fade4ec408d6ead4ecd1dd6 to 4a2986b1179541e2067025d183510bab85a5cf1f. Fast forward Makefile | 9 +++++++++ 1 files changed, 9 insertions(+), 0 deletions(-) create mode 100644 Makefile Y nos deja una historia limpia y lineal: :: o---o---o---x---x "master" = "makeify" Tanto el primer ``pull`` como el ``rebase`` pueden crear conflictos si las ramas tocan los mismos ficheros en zonas muy cercanas en ese caso haríamos lo siguiente: :: $ vim ficheros-que-han-colisionado $ git update-index ficheros-que-han-colisionado $ git am --resolved Explicar estos comandos bien requeriría explicar una de las partes más oscuras de Git, el *index file*. Eso es carne para cortar en otro momento. Publicar nuestro trabajo ======================== Para publicar nuestro trabajo podemos hacerlo de varias formas, usaremos el modelo más simple, lo publicaremos en un servidor donde tenemos cuenta: :: $ ssh miserver miserver$ mkdir git-intro.git miserver$ GIT_DIR=git-intro.git git init-db miserver$ chmod +x git-intro.git/hooks/post-update $ cat << EOF > .git/remotes/miserver > URL: miserver:git-intro.git > EOF Ahora podemos subir nuestra flamante rama ``master`` a *miserver*: :: $ git push miserver master [ ... información del push ... ] Podríamos compartir este repositorio vía web por ejemplo para que otros lo clonaran y nos enviaran sus mejoras. Trucos variados =============== Parches a la Git ---------------- En los proyectos que usan Git está muy de moda utilizar en las listas de correo los parches formateados por varias razones. Una de ellas es por la facilidad de ver qué ficheros han cambiado rápidamente y otra porque utilizando el comando ``applymailbox`` o el comando ``am`` es muy fácil aplicar el parche utilizando la información del autor y la fecha del parche en cuestión. Estos parches los generamos con el comando ``format-patch``, por ejemplo nuestro último parche sería: :: $ git format-patch --stdout HEAD^ 0001-Add-a-clean-target.txt From nobody Mon Sep 17 00:00:00 2001 From: Fernando J. Pereda Date: Wed Apr 12 21:15:05 2006 +0200 Subject: [PATCH] Add a clean target. Signed-off-by: Ego mi mei --- Makefile | 3 +++ 1 files changed, 3 insertions(+), 0 deletions(-) 4a2986b1179541e2067025d183510bab85a5cf1f diff --git a/Makefile b/Makefile index d3a108a..b43f232 100644 --- a/Makefile +++ b/Makefile @@ -4,3 +4,6 @@ OBJS=$(SRCS:.c=.o) ducky: $(OBJS) $(CC) -o ducky $(OBJS) + +clean: + rm *.o ducky -- 1.2.6 Deshaciendo un commit --------------------- Imaginemos que hacemos un commit y nos damos cuenta de que la hemos liado. No pasa nada. Lo primero que haremos será echar la rama un commit hacia atrás: :: $ git reset HEAD^ Makefile: needs update Git nos indica que Makefile no corresponde con el estado actual de la rama. Lo editamos para arreglar el commit y lo volvemos a incluir en el repositorio con el mensaje que tenía en un principio: :: $ vim Makefile $ git commit -C ORIG_HEAD -a Es importante ver que esto CAMBIA el commit ya que como he comentado antes, los objetos son inmutables. Dado que no hemos borrado la rama ``makeify`` esto ha quedado así: :: o---o---o---o---o "master" \ o "makeify" Esto es porque los últimos cambios los hemos hecho sobre ``master`` y esto demuestra que efectivamente, los objetos son inmutables. Como no queremos confusión borraremos ``makeify``. :: $ git branch -D makeify Deleted branch makeify. Manteniendo el repositorio en orden ----------------------------------- Cuando deshacemos muchos ``commit`` s o ``rebase`` s se nos quedarán objetos perdidos por el repositorio. En esta mini-sección veremos unos comandos para mantener el repositorio en orden. Lo primero será comprobar si hay algún objeto perdido: :: $ git fsck-objects dangling commit 4a2986b1179541e2067025d183510bab85a5cf1f dangling commit ea4448c6f9142f3472a2bf21c0a696cb740151c9 Pues es que si. Lo que haremos será crear un pack con todos los objetos actuales que nos sirven para algo y borrar los objetos que no nos interesan. Además esto reducirá el tamaño del repositorio: :: $ du -hs 169K $ git count-objects 22 objects, 88 kilobytes $ git repack -a -d Generating pack... Done counting 15 objects. Deltifying 15 objects. 100% (15/15) done Writing 15 objects. 100% (15/15) done Total 15, written 15 (delta 0), reused 0 (delta 0) Pack pack-e1b94904c2a83b7b0ea2bcb7add651bb6beb0453 created. $ git count-objects 22 objects, 88 kilobytes $ git prune-packed $ git count-objects 7 objects, 28 kilobytes $ git prune $ git count-objects 0 objects, 0 kilobytes $ du -hs 100K Hemos limpiado el repositorio de objetos y hemos ahorrado algo de espacio: :: $ tree .git/objects .git/objects/ |-- info | `-- packs `-- pack |-- pack-e1b94904c2a83b7b0ea2bcb7add651bb6beb0453.idx `-- pack-e1b94904c2a83b7b0ea2bcb7add651bb6beb0453.pack 2 directories, 3 files Deshacer un commit sin perder trabajo a medias ---------------------------------------------- Es realmente fácil hacer esto también. Supongamos que estamos trabajando en una nueva feature de ``ducky``: :: $ vim main.c $ git diff diff --git a/main.c b/main.c index 3a7599d..2c743ae 100644 --- a/main.c +++ b/main.c @@ -3,16 +3,19 @@ int main(int argc, char *argv[]) { int n; + char *msg = "Cuack!"; if (argc < 2) { fprintf(stderr,"%s: Necesito un número!\n",argv[0]); return 1; + } else if (argc == 3) { + msg = argv[2]; } n = atoi(argv[1]); while (n-- > 0) - puts("Cuack!"); + puts(msg); return 0; } Pero nos damos cuenta de que el ``commit`` anterior está mal, ya que ``clean`` debería llamar a ``rm -f`` en lugar de ``rm`` para que no de error si algún fichero no existe. Con lo que hemos comentado antes sería fácil, sin embargo perderíamos lo que hemos hecho hasta ahora y eso no es interesante. Para no perder lo que tenemos hecho crearemos una rama de usar y tirar, haremos un ``commit`` de usar y tirar, arreglaremos el commit anterior y recuperaremos el de usar y tirar: :: $ git checkout -b usarytirar $ git commit -s -m "usar y tirar" -a $ git checkout master $ git reset HEAD^ Makefile: needs update $ vim Makefile $ git commit -C ORIG_HEAD -a $ git cherry-pick -n -r usarytirar First trying simple merge strategy to cherry-pick. Finished one cherry-pick. $ vim main.c $ git commit -s -m "Allow users to specify output message." -a $ git branch -D usarytirar Deleted branch usarytirar. $ git prune Diff avanzado ------------- A medida que el proyecto crece y tenemos muchas ramas nos interesará obtener las diferencias (``diff``) entre una rama y otra. Es decir, queremos los ``commit`` que hay en la rama ``b`` y no hay en la rama ``a``: :: o---o---o---o---o "a" \ x---x---x---x---x "b" Es decir, queremos los ``commit`` marcados con **x**. Podríamos usar una notación especial en Git: ``a..b`` o ``^a b``. :: $ git diff a..b Si estamos actualmente en la rama ``b`` podemos abreviar: :: $ git diff a.. Ser bueno con el mundo ---------------------- Cualquier proyecto que se precie no permitirá parches chapuzas. Para asegurarte de que tus parches no son muy chapuzas utiliza ``hooks/pre-commit`` que hará varias comprobaciones antes de incluir el cambio en el repositorio: :: $ chmod +x .git/hooks/pre-commit .. vim: set ft=glep tw=80 sw=4 et :