--- title: GIT subtitle: Uso avanzado license: CC-BY-NC-SA 4.0 author: Ekaitz Zarraga - ElenQ Technology links-as-notes: true lang: spanish polyglossia-lang: name: spanish how-to: pandoc -f markdown+smart -t beamer % -o pdf/2.pdf --pdf-engine=xelatex --template=./template.tex ... ## Licencia - CC-BY-SA 4.0 - El contenido del documento ha sido adaptado de [**Pro Git (Scott Chacon, Ben Straub)**](https://git-scm.com/book/en/v2). # Git en el servidor ## Git en el servidor: introducción Para compartir el trabajo con el resto del equipo de desarrollo suele utilizarse un servidor: siempre está disponible, facilita la gestión, etc. En los servidores suelen utilizarse lo que se conoce como *bare repository*. Éstos no tienen *working directory*: sólo tienen directorio `.git`. - `git init --bare` construye un *bare repository* > Los *bare repository* suelen nombrarse `loquesea.git` porque sólo contienen > la propia carpeta `.git`, pero siguen siendo directorios, a pesar de su > extensión. ## Protocolos - **Local**: un repositorio que está en otro lugar del sistema de archivos de la máquina: `git clone /path/to/file` - **HTTP**: tiene modos *smart* o *dumb*, el primero siendo capaz de gestionar permisos. - **SSH**: es uno de los modos más utilizados. Necesita unas credenciales:\ `git clone ssh://[@]/.git`\ También se puede usar el modo corto de SCP: `[@]:.git` - **Git**: Similar a SSH pero sin autenticación ## Protocolos — Un ejemplo Mi [servidor](https://git.elenq.tech/): - Está accesible desde la Web mediante `cgit`. Hay otras interfaces web como `gitweb`, que viene con el propio Git. - Los proyectos públicos se pueden clonar sin problema con el protocolo Git. - Para escribir, aporto acceso mediante SSH para tener control de permisos. *En el libro se cuenta cómo montarlos* ## Workflows con Git Git es un sistema distribuido, lo que permite muchos workflows distintos. Sin embargo, hoy en día se usa sobre todo el workflow *centralizado*, debido al éxito de las plataformas web que integran muchas partes del proceso de desarrollo de software (incidencias, releases, etc) junto al control de versiones: GitHub, GitLab, Gitea... ![Los clientes se sincronizan con el servidor](img/centralized_workflow.png){ height=140px } ## Mediante email Muchos proyectos de software libre se basan en el email: - `git format-patch ` crea parches a partir de los commits seleccionados - `git send-email ` envía los parches por email (requiere de configuración adicional) - `git am ` aplica los parches en el proyectos mediante commits - `git apply ` aplica los cambios de los parches sin commitear # Herramientas avanzadas de Git ## Selectores de commits Se pueden aplicar de forma generalizada en muchísimos comandos de Git. - `git show ` para mostrar los objetos seleccionados. > Ver la ayuda de `git show` > Ver la ayuda de `git rev-parse` para leer la sintaxis de los selectores ## Selectores de commits — I - **Hash SHA-1** el propio identificador de un commit es un selector válido. Como son muy largos pueden abreviarse (ver `git log --abbrev-commit`) - **Cabezas de Ramas** (también llamadas *head*), son el nombre de la rama y se resuelven al commit al que la rama apunta. Ejemplo `git show `. Para ver cuál es el commit subyacente `git rev-parse `. *Cuidado, a veces funcionan como cabeza y otras como la rama completa* - **El Reflog** es un registro histórico de los cambios aplicados en Git (similar al undo/redo de cualquier programa). Las entradas del Reflog pueden usarse como selectores. Es interesante porque pueden usar el tiempo: `HEAD@{yesterday}` - `git reflog` para listar las entradas del *reflog*. ## Selectores de commits — II - **Ancestros** de los commits: - El acento circunflejo (`^`) indica el padre del selector utilizado. Por ejemplo: `git show HEAD^`. Los *merge commits* tienen muchos padres, y se puede elegir cuál de ellos con un número después del símbolo `^`. - La vírgula (`~`) sirve para obtener los ancestros del selector. Es similar al anterior, pero al añadir un número se elige la generación, no cuál de sus padres. `git show HEAD~2` elige el padre del padre mientras que `git show HEAD^2` elige el segundo padre. > Usados por separado, son equivalentes ## Selectores de commits — III - **Rango de commits** indicados mediante `..` seleccionan una lista de commits entre los dos selectores indicados en los extremos. ![](img/double-dot.png){width=350px}\ - `git log master..experiment` => D C\ - `git log experiment..master` => F E ## Selectores de commits — IV - Los **puntos triples** actúan como un rango pero seleccionan los commits a los que se puede llegar desde las dos cabezas indicadas pero que las dos ramas no tienen en común. ![](img/double-dot.png){width=350px}\ - `git log master...experiment` => F E D C\ - Para ver de dónde vienen, `--left-right`: ``` git log master...experiment --left-right < F < E > D > C ``` ## Selectores de commits — V - **Puntos múltiples** pueden seleccionarse mediante `--not` o `^` (por delante) para retirar commits de un selector: - Los siguientes comandos son equivalentes: ``` git log refA..refB git log ^refA refB git log refB --not refA ``` - Los siguientes comandos obtienen los commits de `refA` y `refB` pero descartan los de `refC`: ``` git log refA refB ^refC git log refA refB --not refC ``` ## Staging interactivo - `git add --interactive|-i` para gestionar el *stash* de forma interactiva - `git add --patch|-p` para seleccionar los cambios manualmente > Se pueden aplicar en muchos comandos diferentes. Son muy útiles. ## El stash El *stash* es un saco donde guardar cambios no guardados, así se pueden aplicar otros cambios en el repositorio sin mezclarlos. - `git stash [push]` introduce los cambios en el stash y limpia el directorio de trabajo - `git stash list` muestra las entradas del stash - `git stash apply []` aplica los cambios de la entrada del stash seleccionada (por defecto la última) en el *working directory*. - `git stash drop []` borra la entrada del stash seleccionada - `git stash pop` = `git stash apply` + `git stash drop` (sólo aplica el `drop` si el `apply` ha ido bien) Se usan mensajes *push* y *pop* porque las entradas del stash funcionan como una pila (*stack*) ## El stash — opciones interesantes - `git stash apply --index` introduce los cambios en la *staging area* (también se llama *index*). Por defecto no lo hace. - `git stash --keep-index` los archivos indexados (en el *staging area*) no se mandan al stash, se mantienen en el *index* - `git stash --include-untracked|-u` los archivos no-trackeados también se mandan al *stash*. - `git stash --all|-a` los archivos ignorados (`.gitignore`) también se envían al *stash* - `git stash --patch|-p` para seleccionar los cambios a enviar al *stash* de forma interactiva - `git stash branch []` crea una rama nueva y aplica el *stash* en ella. Si todo va bien borra la entrada del *stash* más tarde. ## Para limpiar el directorio - `git stash --all` limpia el directorio enviando todo al *stash*. Luego se puede eliminar haciendo `drop` - `git clean` hace esto mismo, sin pasar por el *stash*. - `-f` (*force*) es necesario porque si no sólo aplica un *dry-run* (una muestra de lo que pasaría sin llegar a aplicarlo) > CUIDADO: aplicar siempre el `--dry-run|-n` en `git clean` primero por si > acaso. ## Firmas Para evitar commits falsos, se pueden firmar por GPG. > GPG (Gnu Privacy Guard), es una implementación de PGP (Pretty Good Privacy) Hay que configurar `user.signkey` - `git commit -S` para firmar el commit - `git tag -s` para firmar etiquetas - `git merge --verify-signatures` comprueba las firmas en el merge - `git tag -v` comprueba las firmas en una etiqueta Si se utilizan firmas, todo el equipo de desarrollo debe utilizarlas. ## Búsquedas - `git grep` para hacer búsquedas mediante expresiones regulares. Similar al comando `grep`, pero es más rápido y busca en el index y el histórico - `git log` es muy potente para buscar en el histórico - `-S` funciones *pickaxe* - `-L` para ver la evolución de una línea de código o una función ## Reescribir la historia - `git commit --amend` es como un mini-rebase - `git rebase -i|--interactive ` cambia los commits necesarios para llegar al selector.\ Abre un editor de texto con los commits seleccionados y permite manipularlos con diversos comandos (los explica en el propio editor) que sirven para fusionar commits, cambiar los mensajes, editar los contenidos... En algunos casos se pausa para editar el contexto, permite continuar mediante `git rebase --continue` o cancelar con `--abort`. El propio proceso es bastante explicativo. - `git filter-branch` es mucho más poderoso pero menos utilizado. Permite cambiar todos los commits de una rama ## Reset y checkout en profundidad — I Para entenderlos en detalle es necesario comprender los estados de los archivos en profundidad. Hay tres secciones disponibles: 1. HEAD: el snapshot del último commit 2. Index: el *staging area*, la propuesta del próximo commit 3. Working directory: el lugar donde realizamos los cambios ![Transiciones entre estados](img/reset-workflow.png){height=150px} ## Reset y checkout en profundidad — II - `git reset` tiene tres modos: - `--soft`: Mover la rama apuntada por HEAD - `--mixed`: `--soft` + pasar los cambios al index - `--hard`: `--mixed` + aplica los cambios también en tu *working directory* ## Reset y checkout en profundidad — III Empezando con un repositorio vacío: ![ ](img/reset-ex1.png) ## Reset y checkout en profundidad — IV Pasar un archivo al *index*: ![ ](img/reset-ex2.png) ## Reset y checkout en profundidad — V Escribirlo en el repositorio: ![ ](img/reset-ex3.png) ## Reset y checkout en profundidad — VI Cambiar un archivo: ![ ](img/reset-ex4.png) ## Reset y checkout en profundidad — VII Mandar los cambios al *index* ![ ](img/reset-ex5.png) ## Reset y checkout en profundidad — VIII Añadir un commit nuevo: ![ ](img/reset-ex6.png) ## Reset y checkout en profundidad — IX Añadir otro commit ![ ](img/reset-start.png) ## Reset y checkout en profundidad — X `--soft`: ![ ](img/reset-soft.png) ## Reset y checkout en profundidad — XI `--mixed` (es el que se aplica por defecto): ![ ](img/reset-mixed.png) ## Reset y checkout en profundidad — XII `--hard`: ![ ](img/reset-hard.png) ## Reset y checkout en profundidad — XIII Además de las tres opciones, se le pueden entregar archivos a `git reset`. En este caso, el primer paso (mover el HEAD) no puede aplicarse del todo[^head] pero el resto pueden aplicarse sin problemas. Esto habilita funcionamientos interesantes: - `git reset ` realmente hace\ `git reset --mixed HEAD `\ 1. ~~Mueve el HEAD~~ 2. Coloca el estado del archivo en el index \ Esto es: **quita el archivo del staging area** [^head]: El HEAD no se puede mover a medias, o se mueve el repositorio completo o no se mueve. ## Reset y checkout en profundidad — XIV A nivel gráfico: ![ ](img/reset-path1.png) ## Reset y checkout en profundidad — XV En un ejemplo más complejo: `git reset -- ` 1. El HEAD no se puede mover 2. Se mueve el estado que `` tenía en `` al index (`--mixed`). ![ ](img/reset-path3.png){height=220px} ## Reset y checkout en profundidad — XVI Puede usarse este concepto para hacer un *squash* (combinar muchos commits en uno) fácilmente [^rebase-interactive]. - `git reset --soft HEAD~` lleva HEAD hacia atrás, manteniendo los cambios en el *index*. Después, se puede hacer `git commit` para aplicar los cambios que quedaron en el *index*, todos de una vez. En **un solo commit**. [^rebase-interactive]: También se puede hacer con `git rebase --interactive|-i` ## Reset y checkout en profundidad — XVII Gráficamente: ![ ](img/reset-squash-r1.png) ## Reset y checkout en profundidad — XVIII Mover el HEAD manteniendo los cambios en el index: ![ ](img/reset-squash-r2.png) ## Reset y checkout en profundidad — XIX Añadir el commit con los cambios que quedaron en el index ![ ](img/reset-squash-r3.png) ## Reset y checkout en profundidad — XX `checkout` y `reset` son similares pero no son iguales: - `git checkout` no manipula el HEAD, sino que lo reasigna a la referencia que se le indique, sin transformar la rama subyacente. - `git checkout ` y `git reset --hard ` son casi iguales, pero el checkout no pisa el directorio de trabajo directamente. - `git checkout ` **pisa el estado del archivo** en el *working directory*, parecido a `git reset --hard`. CUIDADO A ambos se les puede enviar `--patch` para realizarlos por partes. ## Reset y checkout en profundidad — XXI Efecto en el HEAD: ![ ](img/reset-checkout.png) ## Merges avanzados, gestión de conflictos — I Antes de hacer un *merge* es interesante limpiar el *working directory* (`git stash`), de este modo, si algo falla es fácil volver atrás. - `git merge --abort` deshace el merge en caso de conflicto. > CUIDADO: Si hay cambios en el directorio de trabajo, no sabrá abortar. ## Merges avanzados, gestión de conflictos — II En los conflictos, Git aporta 3 archivos: 1. Stage 1: el *common ancestor* (*base*), el commit al que se puede acceder subiendo por ambas ramas. Para verlo puede usarse `git merge-base` 2. Stage 2: Nuestra (*ours*) versión, lo que está en nuestra rama 3. Stage 3: Su (*theirs*) versión, lo que está en la rama que estamos mergeando. - `git show ::` para ver los archivos - `::` obtiene el hash del blob señalado - `git diff` muestra los cambios: - `git diff -1|--base` - `git diff -2|--ours` - `git diff -3|--theirs` ## Merges avanzados, gestión de conflictos — III - `git show ::` se puede utilizar para obtener el contenido de los diferentes archivos y aplicarlos directamente. - `git merge-file` puede utilizarse para aplicar el merge a mano. Aplica algoritmos de merging. ## Merges avanzados, gestión de conflictos — IV Git tiene herramientas mejores: - `git checkout --conflict` te devuelve al estado del conflicto, con los marcadores, para deshacer los cambios que se hayan podido realizar por error al tratar de corregir el conflicto - `git checkout --ours/--theirs` para ver uno o otro estado - `git log --oneline --left-right --merge` facilita mostrar el contexto - `git diff` muestra un *combined diff* cuando se aplica en un conflicto - `git show -p|--patch` junto con `--cc` muestra el *combined diff* ## Merges avanzados, gestión de conflictos — V Este es el aspecto de un *combined diff*: ``` diff --cc hello.rb index 0399cd5,59727f0..0000000 --- a/hello.rb +++ b/hello.rb @@@ -1,7 -1,7 +1,7 @@@ #! /usr/bin/env ruby def hello - puts 'hola world' - puts 'hello mundo' ++ puts 'hola mundo' end ``` Muestra dos columnas con símbolos `+` y `-`, mostrando qué ha ocurrido en cada lado del merge. ## Merges avanzados, gestión de conflictos — VI Puede ser interesante usar `diff3` para los conflictos. Por defecto se usa `merge`. `diff3` muestra una parte adicional en el conflicto, que muestra la *base*. ``` <<<<<<< ours puts 'hola world' ||||||| base puts 'hello world' ======= puts 'hello mundo' >>>>>>> theirs ``` - `git checkout --conflict=diff3` para mostrar el conflicto en 3 partes - `git config --global merge.conflictstyle diff3` recomiendo configurarlo para que funcione así siempre ## Deshaciendo merges — I Dos modos: 1. `git rest --hard HEAD~` (sobreescribe el histórico) 2. `git revert -m 1 HEAD` añade un commit que deshace los cambios. Esto da problemas a la hora de volver a combinar la rama. ## Deshaciendo merges — II `git revert -m 1 HEAD` añade un commit que deshace los cambios. `^M` y `C6` tienen los mismos contenidos, pero los cambios en `topic` pueden accederse desde `master`, para Git, ambas están mergeadas. CUIDADO ![](img/undomerge-revert.png) ## Deshaciendo merges — III Lo que es peor, si se siguen añadiendo cambios en `topic` y se intenta hacer un merge nuevo, Git no añadirá los cambios previos, pensando que ya fueron combinados (y lo fueron) pero sin saber que se deshicieron. CUIDADO ![](img/undomerge-revert2.png) ## Deshaciendo merges — IV Para evitar el problema, hay que deshacer de nuevo el commit que se deshizo. Esto puede hacerse con un nuevo `revert`. ![](img/undomerge-revert3.png) Ahora todos los cambios de `topic` se verán en `master`. ## Preferencias en los merges `git merge` tiene muchas opciones y estrategias: - `-X` añade opciones. Por ejemplo `-Xours` resuelve los cambios a nuestro favor. - `-s` selecciona estrategias. Por ejemplo `-s ours` en lugar de hacer un merge sólo se queda con nuestros cambios. Es interesante para engañar a Git. Las estrategias no son lo mismo que las opciones. Las estrategias *seleccionan* qué algoritmo de merge utilizar. Las opciones *configuran* el algoritmo. ## Subtree Los subtrees son un mecanismo para introducir otro proyecto dentro del actual. Este sub-proyecto funciona como una rama independiente: se añade como un remoto independiente en una rama suelta, que no tiene relación con las otras. - `git read-tree` para leer el subproyecto e inicializarlo en una rama independiente - `git merge -Xsubtree` para combinar (*merge*) la rama del subproyecto en el proyecto actual sin problemas - `git diff-tree` para analizar cambios en el subproyecto, un `diff` normal no funcionaría *No se utiliza mucho* porque hoy en día se usan los *submodules* en su lugar. Pero es interesante para ver lo flexible de los métodos de merge. ## Rerere: Reuse Recorded Resolution — I El rerere es un mecanismo para guardar soluciones a conflictos, y enseñar a Git a resolverlos cuando vuelvan a darse. Por ejemplo: 1. Al mergear una rama de test aparecen conflictos 2. Se arreglan los conflictos 3. Los tests van mal 4. Se deshace el merge 5. Se arreglan los tests 6. Se vuelve a hacer el merge => los mismos conflictos reaparecen Con el **rerere** se podía haber guardado la resolución en el paso 1, de esta manera al aplicar el paso 6 Git podía haber recordado la solución del paso 1 y volver a aplicarla sin necesitar interacción humana. ## Rerere: Reuse Recorded Resolution — II - `git config --global rerere.enabled true` - `git rerere status` - `git rerere diff` para ver los cambios que aplicaría rerere - `git add` + `git commit` guarda la resolución del conflicto:\ `"Recorded resolution"` - `git reset --hard HEAD^` volver atrás el en el tiempo, y volver a hacer el merge resuelve el conflicto automáticamente:\ `"Resolved with previous resolution"` - `git checkout --conflict=merge ` recupera el conflicto, desactivando el rerere - `git rerere` se puede aplicar después el rerere automáticamente ## Debugging con Git - `git blame|annotate` muestra qué commit introdujo las líneas del archivo indicado. Las líneas que empiezan con `^` fueron introducidas con la creación del archivo. Usar `-C` intenta buscar los bloques que han sido movidos[^move]. [^move]: Git no guarda record de los movimientos pero aplica algoritmos de búsqueda que pueden calcular este tipo de cambios. - `git bisect` aplica el [método de la bisección][bisect] para buscar commits que introdujeron errores en el repositorio. Método: - Tomar dos commits: uno con error y otro sin él - Toma un commit en el centro de los dos: si tiene error, el commit que introdujo el error en el repositorio estará entre éste commit y el que no tenía error del paso anterior. Si no tiene error, será al revés. - Con el nuevo rango, repite el algoritmo tomando nuevos commits cada vez, hasta encontrar el commit que introdujo el fallo. [bisect]: https://en.wikipedia.org/wiki/Bisection_method ## Debugging con Git: `git bisect` Pasos a aplicar: 1. `git bisect start` empezar 2. `git bisect bad` indicar commit actual como erróneo 3. `git bisect good ` indicar último commit que se tiene constancia de que era correcto 4. `git bisect good/bad` Git elegirá commits y saltará a ellos para que los marquemos como `good` o `bad`:\ `"Bisecting, N revisions left to test"` 5. `git bisect reset` volver al inicio Se puede automatizar con `git bisect run `. El comando debe retornar `0` en casos de acierto y cualquier otro valor cuando falle. ## Submodule Son un modo interesante para gestionar subproyectos en el repositorio. - `git submodule add ` crea un nuevo subproyecto e inicializa el archivo `.gitmodules` - `.gitmodules` es un archivo que guarda los la información de los subproyectos: la URL, el nombre y el path. - `git submodule add ...` seguido de `git diff --cached` muestra que el contenido de un submodulo no se gestiona como un archivo cualquiera:\ `"Subproject commit ---"` - `git diff --submodule --cached` muestra mejor el contenido - `git push` añade el submódulo pero como una referencia a otro repositorio, no como archivo ## Clonar submódulos - `git clone` **no** clona los submódulos automáticamente - `git submodule init` inicializa el control de submódulos - `git submodule update` para actualizar los archivos de los submódulos - `git clone --recurse-submodule ` descarga automáticamente todos los submódulos (recursivamente) al hacer `clone`. Si no se hace, debe lanzarse `git submodule update --init` o `git submodule update --init --recursive` más tarde. ## Trabajar con submodulos - `git config --global diff.submodule log` para no tener que añadir cada vez `--submodule` que se hace `git diff` - `git submodule update --remote ` para actualizar los contenidos del submódulo. También se puede hacer `fetch` + `merge` dentro del submódulo. - `git config -f .gitmodules submodule..branch ` para cambiar el submódulo de rama. Equivalente a editar el archivo `.gitmodules` - `git config status.submodulesummary 1` para que `git status` muestre el estado de los submódulos. - `git log --submodule` para mostrar el listado de cambios de los submódulos ## Publicar cambios de los submódulos - `git push` dentro del submódulo publica sus cambios (son un repositorio normal) - `git push --recurse-submodules=check|on-demand` hay dos estrategias. `check` comprueba si deben pushearse cambios, mientras que `on-demand` pushea automáticamente los submódulos si hay cambios en ellos. - `git config push.recurseSubmodules check|on-demand` para configurarlo por defecto ## Trucos con submódulos - `git submodule foreach ` lanza un comando en cada submódulo. Ejemplo:\ `git submodule foreach 'git stash'` ## Bundle Son archivos que guardan conjuntos de commits o repositorios completos de modo que son fáciles de compartir. - `git bundle create []` guarda los commits seleccionados en el archivo ``. Normalmente es interesante introducir `HEAD` para que luego el bundle sepa dónde estaba el repositorio. - `git clone ` para clonar desde un bundle. Necesita que el bundle contenga todos los commits del repositorio. - `git bundle verify ` para comprobar si un bundle parcial se puede aplicar en el el repositorio actual - `git bundle list-heads ` para ver las ramas en el archivo - `git fetch/pull ` permite aplicar commits desde el archivo bundle ## Replace La base de datos de Git no puede alterarse, pero con `git replace` puede hacerse que parezca que ha cambiado. - `git replace ` hace que cuando se use `` funcione como si se hubiese usado `` en su lugar. *No es común, investigar en el libro posibles casos de uso* ## Sistemas de credenciales Al usar credenciales sobre HTTP Git necesita que todas las operaciones con el servidor usen usuario y contraseña. Este comportamiento puede configurarse: - `git config --global credential.helper ` 1. Pedir siempre (lo hace por defecto)) 2. `cache` mantener las credenciales en memoria 15 minutos 3. `store` guardar las credenciales Hay más opciones (`--timeout`, sistemas de credenciales a medida, etc.) # Configuración avanzada ## Configuración Ver documentación sobre la configuración `git help git-config` - Editor y paginador: `core.editor` y `core.pager` - Plantillas para commits: `commit.template` - Herramientas de merge y diff: `merge.tool` y `diff.tool` - Control de espacios en blanco: `core.autocrlf` y `core.whitespace` ## Git attributes — I Dentro del proyecto, a nivel de archivo, se puede cambiar el comportamiento de Git mediante los atributos (*attribute*). - `.gitattributes` o `.git/info/attributes`, el primero se puede compartir en el repositorio, el segundo no. El archivo `.gitattributes` es similar a `.gitignore`, además de las plantillas que seleccionan los archivos se definen unos atributos para ellos. Muchas veces se requiere de configuración adicional para los atributos mediante *drivers*. Ver `git help attributes` ## Git attributes — II Hay muchos atributos diferentes, los más interesantes: - `binary` para tratar un archivo como binario - `diff=` pasa el archivo por el filtro indicado antes de hacer diff. Necesita configuración adicional: - `git config diff..textconv ` - Por ejemplo: `*.png diff=exif` - `git config diff.exif.textconv exiftool`\ para comparar los resultados de ejecutar `exiftool` sobre el archivo, en lugar de sus contenidos - `ident` añade identificación. Sustituye el texto en `$Id$` por el SHA-1 del commit actual. ## Git attributes — III - `filter` pasa los archivos por un filtro - `smudge` aplica el filtro cuando el archivo pasa del *index* al *working directory* - `clean` en el camino opuesto - Ejemplo: `*.c filter=ident` - `git config filter.ident.clean indent`\ `git config filter.ident.smudge cat`\ indenta los archivos automáticamente al hacer `git add` (a la vuelta no aplica nada) - `export-ignore` ignora archivos al hacer un archivado del repositorio (`git archive`) - `export-subst` aplica las expansiones que `git log` aplicaría y sustituye patrones en los archivos - `merge` selecciona estrategias de merge por archivo ## Git hooks Los hooks son programas que se ejecutan en momentos importantes del uso del repositorio. Se usan muy a menudo. - `.git/hooks` es el directorio donde se guardan, deben usar un nombre en particular y tener permisos de ejecución Por defecto, se instalan varios hooks al hacer `git init` pero están desactivados con `.sample` al final de su nombre. Se instalan para servir de muestra. ## Hooks del lado del cliente Para uso privado. **No se copian junto al repositorio**[^compartir-hooks]. Son interesantes como alertas para quien use el repositorio: que los tests no pasan, que hay un problema en los formatos, etc. Algunos interesantes: - `pre-commit` se ejecuta antes de escribir el mensaje del commit. Se suele utilizar para comprobar si se ha olvidado algo. - `prepare-commit-msg` procesa el mensaje de commit por defecto antes de mostrárselo al usuario. Sirve para añadir información adicional en el mensaje, cambiar la plantilla, etc. - `commit-msg` procesa el contenido el mensaje del commit. Si el mensaje del commit no es correcto puede rechazarse. Se utiliza para asegurar que el mensaje de commit usa la forma adecuada. [^compartir-hooks]: Hay herramientas que facilitan el intercambio de hooks, pero no están incluidas en Git. ## Hooks del lado del servidor Son interesantes para imponer políticas de desarrollo, ya que pueden rechazar `push`es si estas políticas no se cumplen. Algunos interesantes: - `pre-receive` se lanza al recibir un `push`, recibe todas las referencias y si da error no se actualizarán los datos del servidor - `update` similar al anterior, pero se ejecuta una vez por cada rama - `post-receive` se ejecuta después de haber recibido todo bien, puede usarse como notificación # Git por dentro ## Git por dentro: introducción Git es una herramienta de bajo nivel y tiene comandos para trabajar a este nivel conocidos como *plumbing*. Los que se han utilizado hasta ahora se conocen como *porcelain*[^baño]. Git es una base de datos *content-addressed*. Sus elementos se guardan por clave y valor (*key-value*) donde la clave se obtiene del valor. Hasta ahora se ha visto: se usan los hashes SHA-1 de los contenidos del commit para identificarlos. El contenido de la base de datos se almacena en el directorio `.git`. Donde se almacenan elementos de diferentes tipos. [^baño]: En un baño, la porcelana está por fuera, las tuberías por dentro ## Tipos de datos Hay dos tipos de dato principales en la base de datos de Git: - Objetos: que tienen contenidos: commits, blobs, árboles, etc. - Referencias: son elementos que apuntan a otros: ramas, etiquetas ligeras, etc. ## Objetos Se almacenan en `.git/objects` usando sus hashes como identificador. Los primeros dos caracteres como nombre de directorio.\ Por ejemplo: `.git/objects/cf/6cbb8a400c7cad0f7f93610366c3672f598cdd` - `git hash-object -w` para escribir nuevos contenidos, devuelve el hash del objeto escrito - `git cat-file` para leerlos, `-p` para mostrarlo bonito. ## Tipos de objetos `git cat-file -t` muestra el tipo - `blob` para guardar los contenidos de los archivos u otro tipo de datos - `tree` para guardar directorios o los nombres de los archivos\ Contienen entradas que apuntan a otros `tree`s o `blob`s identificándolos por su hash. - `commit` para guardar los datos de los commits. Contiene: un `tree` con el snapshot, el mensaje, la autoría, etc. ## Tipos de objetos: tree ![](img/data-model-1.png) ## Tipos de objetos: commit ![](img/data-model-3.png) ## Referencias Sólo contienen un identificador - `git update-ref` puede actualizar su contenido. También se puede realizar manualmente Hay diferentes tipos de referencias: - Tag: Se guardan en `.git/refs/tag`. Cualquier objeto puede etiquetarse (normalmente sólo se hace con los commits). Hay dos tipos: - Lightweight tag: sólo son una referencia - Annotated tag: son un **objeto** y una referencia a éste - Ramas: en `.git/refs/heads`. Hacen referencia a un commit - Remotos: en `.git/refs/remotes` similares a las ramas pero no se pueden cambiar. - HEAD: `.git/HEAD`. Apunta a una rama, pero puede apuntar a un commit (*detached HEAD*). ## Referencias: ramas ![](img/data-model-4.png) ## Ejercicio: notas - `git notes` gestiona las notas *Ver qué son, y cómo funcionan: `git help git-notes`* ## Packfiles Para que el almacenamiento de archivos sea más eficiente Git utiliza sistemas de compresión de archivos, pero no es suficiente. Git se basa en snapshots, pero no guarda los archivos una y otra vez en cada commit, sino que guarda sólo aquellos que han cambiado, y por partes. Además, agrupa los archivos en *packfile*s para agilizar el proceso. - `.git/objects/pack` - `git gc` (*garbage colector*) agrupa los archivos y los comprime (git lo ejecuta automáticamente) - `git verify-pack` comprueba el contenido de un packfile ## Mantenimiento y recuperación de datos - `git gc --auto` reorganiza los packfiles - `git reflog` almacena los cambios realizados en HEAD haciendo que los commits perdidos puedan recuperarse desde éste. - `git fsck --full` muestra los elementos perdidos, permitiendo también la recuperación. *EJERCICIO: si se añade un archivo grande por accidente, `git rm` no lo elimina, por lo que se convierte en un problema. ¿Cómo se borra? ¿Por qué?* ## El refspec Para gestionar el mapeo entre las ramas remotas y locales. Éste es el contenido de `.git/config` tras hacer `git remote add origin https://github.com/schacon/simplegit-progit`: ``` [remote "origin"] url = https://github.com/schacon/simplegit-progit fetch = +refs/heads/*:refs/remotes/origin/* ``` El bloque `fetch` indica mediante el refspec cómo se deben gestionar las ramas. Puede haber muchas entradas, con diferentes reglas. Formato: - Un `+` indica (opcional) si deben actualizarse incluso cuando no es *fast-forward* - `:` indica cómo mapear las ramas. Ambos son patrones. Puede usarse `*` para indicar «todo» ## El refspec manualmente Pueden usarse patrones al hacer `fetch` o `push` para indicar este aquí y este allá. Git aplica expansiones basándose en el refspec: - `git log origin/master`\ => `git log remotes/origin/master`\ => `git log refs/remotes/origin/master` Antes, para borrar las ramas había que usar el refspec: - `git push :` al añadir la fuente vacía, en el remoto se borra la rama - Ahora puede usarse `git push --delete `