--- title: GIT subtitle: Introducción y uso básico 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/1.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). # Introducción: sistemas de control de versiones ## Sistemas de control de versiones `Document_v3_FINAL.pdf` --- ![Centralizados](img/zentralizatuak.png){ height=180px } --- ![Descentralizados](img/banatuak.png){ height=300px } # Git: Introducción ## Contexto - 2005 - Para el desarrollo de Linux - Miles de desarrolladores - Código fuente muy extenso - Es distribuído (*distributed*) - Se basa en *snapshots* - La mayoría de las operaciones ocurren en local y no son destructivas - Control de integridad mediante SHA-1 (o SHA-256) - Tres estados:\ `ARCHIVO — STAGING AREA — REPOSITORIO` ## Instalación Debian: ``` apt-get install git ``` En el resto de distros es similar. ## Sistema de configuración - Sistema: `/etc/gitconfig` - Configuración general: `~/.config/git/config` o `~/.gitconfig` - Local por repositorio: `$REPOSITORIO/.git/config` Funciona en modo cascada, de local a sistema. Para ver de dónde se toman los valores: ``` git config --list --show-origin ``` ## Gestión de configuración Los comandos leen y escriben los archivos de configuración de forma ordenada, aunque se puede hacer a mano: ``` git config [--global]
. [] ``` - El `` es opcional. Si se usa, se escribe la configuración con ese valor. Si no se usa, se devuelve el valor en esa línea de la configuración. Realmente es más complejo que esto: ver documentación. ## Configuración inicial Definir la identidad para que se registre el autor de los commits: ``` git config --global user.name "John Doe" git config --global user.email johndoe@example.com ``` Seleccionar editor de texto por defecto: ``` git config --global core.editor vim ``` ## Ayuda - `git help` - `man` # Git basico ## Obtener un repositorio Dos alternativas: - Crear un repositorio nuevo: `git init`. Esta opción crea un directorio `.git` en el directorio actual, donde se almacenará la información interna de Git. - Clonar un repositorio existente: `git clone `. Esta opción copia el repositorio en el directorio actual, incluyendo su `.git`. Puede usar varios protocolos diferentes ## Estados de un archivo ![Ciclo de vida de los archivos](img/lifecycle.png){ height=180px } ## Visualizar estado de un repositorio - `git status` - Para ignorar archivos, añadirlos a un archivo `.gitignore`. Utiliza *glob pattern*s. ## Para cambiar de estado - Añadir archivos con: `git add` - Para enviar los cambios al *staging area*: `git add` ## Para ver cambios - `git diff` - `git diff --cached|--staged` - `git difftool` (si está configurada) La salida de `git diff` puede guardarse a archivo y después aplicarse sobre el repositorio mediante `git apply`. ## Para escribir los cambios - `git commit` Pide rellenar un mensaje mediante el `$EDITOR` o el `git config --global core.editor` ## Borrar archivos - `git rm` - `git rm --cached` ## Renombrar - `git mv` Equivalente a `git rm` + `git add`. ## Para ver la historia - `git log` Es un comando muy complejo. - `-` Muestra los últimos N commits. - `-p/--patch` muestra el *patch*. - `--stat` muestra estadísticas. - `--pretty` cambia el formato de salida, tiene muchas opciones. - `--graph` modo gráfico. Se pueden combinar: `git log --graph --pretty=oneline --decorate --all` ## Filtrar la historia La salida de `git log` puede filtrarse: - `--since` desde cuando, por ejemplo: `--since=2weeks` - `--author` filtrar por autor - `--grep` búsqueda por palabras clave - `-S/-G/...` *pickaxe function*s, buscan palabras en los cambios aplicados en el commit. - `git log -- archivo` mostrar commits que afectaron a ese archivo. - `--no-merges` descartar commits de tipo *merge* Ver la ayuda: `git help log` ## Deshacer cambios - `git commit --amend` reescribe el último commit - Cambiar el mensaje - Añadir o quitar archivos - ... - `git reset` quita cambios de la *staging area*. CUIDADO con `--hard`. - `git checkout` para deshacer los cambios. CUIDADO - `git restore` comando nuevo (>2.23.0), parecido a los anteriores. Los comandos `reset` y `checkout` son parte integral de Git y realizan más acciones que las que se acaban de mencionar. Se estudian más adelante en detalle. ## Remotos (*remote*) Son copias de un repositorio. Se pueden actualizar enviándoles cambios locales mediante *push* o se pueden descargar cambios desde ellos para actualizar el repositorio local mediante *fetch* y *pull*. Pueden ubicarse en otras máquinas y accederse mediante la red, en otras partes del mismo disco duro, etc. ## Gestión de remotos - `git remote` Están escritos en `.git/config`. También pueden editarse a mano. No se recomienda hacerlo. - `git clone` añade el remoto llamado `origin` automáticamente, apuntando al repositorio del que se clonó. - `git remote -v` - `git remote add ` - `git remote show ` - `git remote rename ` - `git remote remove ` ## Intercambiar información con el remoto - `git fetch []` descarga la información del remoto sin alterar el repositorio local. - `git pull` aplica un `fetch` seguido de un `merge` si las ramas están configuradas correctamente:\ ``` git pull = git fetch + git merge ``` - `git push [ ]` sube los cambios al remoto de forma segura. En caso de conflicto los rechaza. ## Etiquetas (*tag*) Nombres que se les pueden asignar a los commits. Sirven para identificar los commits con nombres fáciles de recordar, son útiles para señalar *releases*. - `git tag -l` muestra las etiquetas Hay dos tipos de etiquetas: - **Lightweight**: son referencias, como una rama fija. Sólo son un identificador. - **Annotated**: son un objeto más en Git, como un commit, y pueden tener autor, firma, mensaje, etc. ## Annotated tags - `git tag -a []` Se muestran en `git show`. ## Lightweight tags - `git tag []` `git show` no las muestra, sino que muestra el commit al que hacen referencia. ## Compartir tags - `git push [] ` - `git push [] --tags` para enviar todos ## Eliminar tags - `git tag -d ` - `git push [] --delete ` ## Aliases Permiten escribir nuevos comandos usando los que ya están disponibles en Git como base. Son valores de la configuración: - `git config --global alias.co checkout` - `git co` => `git checkout` Yo uso mucho: ``` git config --global alias.lg log --graph \ --decorate --all --oneline ``` # Git básico: Ramas ## Rama (*branch*) Las ramas permiten al desarrollo del repositorio tomar diferentes caminos sin alterar el desarrollo principal. En Git las ramas son fundamentales, ya que está diseñado alrededor de éstas. En otros sistemas de control de versiones el uso de ramas es pesado, pero en Git es muy ligero y no es raro ver repositorios con cientos de ramas distintas. ## Git por dentro - Cuando los archivos se mandan al *staging area* Git guarda sus contenidos en un *blob* y calcula su *checksum* (SHA-1) - Cuando se aplica un commit, calcula los checksums de todos los directorios y construye un objeto *tree* por cada uno de ellos. Los objetos *tree* apuntan a los objetos *blob* indicando su nombre, y formando la estructura de archivos. El *tree* principal se añade al commit, junto con el autor, fecha, mensaje y otros datos. Se almacena el objeto *commit* resultante. - Los *commit* hacen referencia a sus *commit* padre. Los commits "normales" tienen un único padre, los commits de tipo *merge* tienen varios padres y el commit inicial no tiene padre. Las ramas son referencias móviles que apuntan a los *commits*. ## Git por dentro ![Datos internos](img/commit-and-tree.png) ## Git por dentro ![Estructura de commits](img/commits-and-parents.png) ## Crear ramas - `git branch ` Por defecto hay una rama llamada `master` (puede configurarse), y siempre debe haber al menos una rama, donde se irán añadiendo los commits nuevos. Git guarda una referencia que apunta a la rama actual: `HEAD` ## Cambiar de rama - `git checkout ` - `git checkout -b ` crear rama y saltar a ella Como `checkout` es un comando complejo se ha añadido el comando `switch` que cumple esta tarea de forma más clara: - `git switch ` - `git switch -c ` crear rama y saltar a ella - `git switch -` saltar a la rama anterior ## Ramas y cambios — I ![`git branch testing`](img/head-to-master.png) ## Ramas y cambios — II ![`git checkout testing`](img/head-to-testing.png) ## Ramas y cambios — III ![`git commit ...`](img/advance-testing.png) ## Ramas y cambios — IV ![`git checkout master`](img/checkout-master.png) ## Ramas y cambios — V ![`git commit ...`](img/advance-master.png) ## Ramas y cambios — VI Visto desde la interfaz de Git: ``` $ git log --oneline --decorate --graph --all * c2b9e (HEAD, master) Made other changes | * 87ab2 (testing) Made a change |/ * f30ab Add feature #32 * 34ac2 Fix bug #1328 * 98ca9 initial commit ``` > CUIDADO: Si no se añade `--all` no se muestran todas las ramas al hacer `git > log`, sino que sólo se muestra la rama actual. ## Ramas y merges — I ![](img/basic-branching-4.png){ height=180px } ## Ramas y merges — II Caso **fast forward**, una rama contiene la otra ![`git checkout master`\ `git merge hotfix`](img/basic-branching-5.png){ height=200px } ## Ramas y merges — III Siempre no es así de fácil: ![](img/basic-branching-6.png){ height=180px } ## Ramas y merges — IV Se busca el ancestro común (*common ancestor*) y se aplica un *three-way merge*. ![`git checkout master`\ `git merge iss53`](img/basic-merging-1.png){ height=180px } ## Ramas y merges — V Crea un commit nuevo que combina ambas ramas. Es un *Merge*: tiene al menos dos padres. ![`git checkout master`\ `git merge iss53`](img/basic-merging-2.png){ height=180px } ## Conflictos — I El caso de antes no siempre sale bien. Es posible que haya un conflicto si se han editado los mismos archivos en las dos ramas. ``` $ git merge iss53 Auto-merging index.html CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result. ``` ## Conflictos — II ``` $ git status On branch master You have unmerged paths. (fix conflicts and run "git commit") Unmerged paths: (use "git add ..." to mark resolution) both modified: index.html no changes added to commit (use "git add" and/or "git commit -a") ``` ## Conflictos — III El archivo se ve así: ``` <<<<<<< HEAD:index.html ======= >>>>>>> iss53:index.html ``` Aparecen unos separadores: `<<<<<<<`, `=======` y `>>>>>>>`. En el primer trozo aparece el contenido del `HEAD` y en el segundo `iss53`. Si está configurada una herramienta, puede usarse `git mergetool` ## Gestión de ramas - `git branch` muestra las ramas - `git branch --merged | --no-merged` muestra las ramas mergeadas o las no mergeadas con la actual. - `git branch -d ` borra ramas. Las no-mergeadas no las borra, hay que hacer `-D` para eso (diferencia mayúsculas y minúsculas). - `git branch --move ` cambiar nombre de rama - `git push -u|--set-upstream ` asigna una rama a un remoto - `git push --delete ` borra una rama en un remoto ## Workflows habituales - *Long-running branches*: `master`, `testing` y `development` - *Topic branches*: por cada issue o feature se trabaja en una rama independiente. ## Ramas en los remotos Git guarda referencias a los remotos para poder ver su estado (`git ls-remote ` o `git remote show` para verlo). Las ramas en los remotos pueden verse con el nombre `/`. No son ramas normales, sino que se muestran como tal. La diferencia es que estas ramas no se pueden alterar de forma directa. La forma de manipularlas es hacer cambios en el remoto, para que éstos se reflejen en la rama. ![Ejemplo: el repositorio local tiene dos commits nuevos](img/remote-branches.png) ## Hacer push - `git push ` envía la rama al remoto. Hay que hacerlo manualmente para no subir más que la rama que se quería. Facilitando el uso de ramas locales. - `:` para usar diferente nombre en local que en el remoto. ## Tracking branches Es una forma de relacionar una rama remota (*upstream*) y una local (*tracking*): - `git checkout -b /` relaciona `/` con `` - `git checkout --track /` relaciona la rama actual con la remota. - `git checkout ` si la rama local `` no existe, se crea relacionada con `/` automáticamente. - `git clone` crea una rama local relacionada con la remota automáticamente. - `git branch -u|--set-upstream-to /` también relaciona las ramas. Ver `git push` > Una vez relacionadas, para referirse a la rama remota puede usarse > `@{upstream}` o `@{u}` ## Hacer pull Si hay una rama de tracking configurada, hacer `git pull` es equivalente a hacer `git fetch` seguido de un `git merge`. Cuidado: a veces es difícil ver qué ocurre con el `merge`. Para evitar problemas, hacer `fetch`, ver el estado del repositorio y hacer el `merge` manualmente. ## Ramas y rebases — I ![](img/basic-rebase-1.png) ## Ramas y rebases — II ![Al hacer `git merge` pasa esto](img/basic-rebase-2.png) ## Ramas y rebases — II ![`git checkout experiment`\ `git rebase master`](img/basic-rebase-3.png) Ahora el merge es *fast-forward* y no añade commit de merge. ## Un rebase más complejo — I ![](img/interesting-rebase-1.png) ## Un rebase más complejo — II ![`git rebase --onto master server client`](img/interesting-rebase-2.png) Pasa los cambios de `client` a `master` sin incluir los de `server`. Ahora se puede hacer un rebase de `server` en `master` y después hacer un `merge` *fast-forward*. ## Cuidado con los rebase Al hacer rebase, se crean commits nuevos con contenidos similares a los que había anteriormente, pero con diferente valor. Al enviarlos a un remoto (`push`) se fuerza al resto de nuestro equipo a hacer rebases: - `git push --force` reescribe la historia en el remoto. Es peligroso. - `git pull --rebase` puede ayudar a la hora de hacer pull de un repositorio en el que hayan ocurrido rebases. Se puede configurar Git para que siempre se comporte así en el pull. ## Rebase vs Merge En función de la filosofía con la que se trabaje es interesante usar uno u otro. - Si se entiende el repositorio como un histórico de cambios el *rebase* no tiene sentido, porque puede manipular el pasado. (*El sistema de control de versiones fossil usa esta filosofía, y no tiene forma de hacer rebases*) - Si se entiende el repositorio como un *making-of*, tiene sentido aplicar rebases, porque limpian la historia haciéndola más fácil de leer. Consejo: En local todo vale. En el servidor compartido cambiar la historia es peligroso porque afecta al equipo de desarrollo, usuarios, etc.