Me salgo por un post del mundo maravilloso del SEO para tener en el blog una guía práctica y rápida de Git. Veréis que voy muy a saco porque no son más que los apuntes de un curso de CodelyTV pero espero que todo se entienda bien.
Primeros comandos
Primeros pasos
Una vez instalado Git usaremos git config para setear parámetros por defecto
git config --global user.name "username" git config --global user.email "username@domain.com"
Para crear un repositorio de 0, dentro del directorio donde queramos versionar
git init
Para ver el estado de los ficheros de la rama donde estamos
git status
Para añadir los ficheros que no están controlados, normalmente los nuevos ficheros
git add nombre_fichero
Para hacer el commit y enviar al staging area los ficheros finales git commit.
Lo normal es hacerlo ya añadiendo el mensaje del commit que deberá ser descriptivo
git commit -m "Descripción del commit"
Para ver todas las versiones que hay
git log
Para tener un repositorio de GIT en la nube, hay que crear un repositorio en, comunmente, GitHub o GitLab y luego ejecutar en local el string id que te dan
git remote add origin git@github.com:username/example-git.git
y luego subir lo que tenemos en local y crear la rama master en remoto de origin
git push -u origin master
Para subir los cambios de un commit utilizaremos git push. Para subir al repositorio origin la rama master
git push origin master
Otros comandos básicos: stash, branch, checkout, tag, revert
git stash
Los ficheros que están por comitear se mueven a una zona diferente para guardarse. De esta manera puedes cambiar de tarea sin perder los cambios que estabas haciendo
Luego se puede hacer un unstash para deshacerlo.
Para asegurar que los nuevos ficheros creados y no trackeados se guarden en la mochila, hay que hacer un git add para pasarlos al staging area o bien utilizar
git stash -u
Como es una pila, para deshacer al cambio anterior se puede hacer un
git stash pop
Finalmente, para recuperar una mochila determinada bastaría con listar los cambios y seleccionar uno de ellos
git stash list
git stash pop [numid]
git branch y git checkout
Para crear rama
git branch nombre_de_rama
Para eliminarla
git branch -d nombre_de_rama
Para listar las ramas
git branch
para cambiar de rama o branch
git checkout nombre_de_rama
git tag
Un tag es una versión fija del repositorio. Es poner un alias al hash de un commit como referencia, normalmente para entender el commmit como versión.
git tag "v1.0.0"
Si hacemos un git log veremos los hash de los diferentes commits. Podemos coger ese hash y hacer el tag
git tag -a v1.2 9fceb02
Para listar los tags basta con git tag y si nos hemos equivocado, podemos eliminarlo
git tag -d v1.4
git revert
Se utiliza para revertir los cambios de un commit. Es un nuevo commit en realidad que vuelve a un estado anterior.
Hay que señalar el hash del commit que se quiere revertir. Para obtenerlo, de nuevo, basta con git log.
git revert num_hash
Conceptos del trabajo en local
git add vs git commit
Si creamos nuevos ficheros tenemos que utilizar git add para que suban los cambios en el commit.
Si modificamos ficheros ya existentes, desde el mismo commit se pueden añadir con -a.
git commit -am "Diferencias entre git add y git commit"
git chechout vs git branch & tips
Con checkout te puedes mover entre ramas e incluso entre commits mientras que git branch solo sirve para listar y crear ramas.
Hay un comando con checkout que permite crear la rama si ésta no exite, con lo cual ahorraríamos un paso a la hora de crear una rama y movernos a ella
git checkout -b nombre_rama
Para ver las ramas en remoto
git branch -a
Conceptos del trabajo en remoto
fetch vs pull & tips
git fetch
El fetch va a ser el comando que traiga las ramas al índice que hay en el repositorio y que no tenemos en local. Para actualizar nuestras ramas con las de origin
git fetch origin
Para borrar las ramas en local que ya no existan en remoto
git fetch -ap
git pull
En realidad git pull es como un alias de dos comandos: git fetch + git merge.
Con git pull actualizamos en nuestra rama los cambios que pudieran haber a nivel de código pero también actualiza el índice local de ramas.
Productividad en git
Alias de git
Tiene que quedar claro que no son alias de consola, son alias exclusivos de git. Por ello, para insertarlos hay que editar el fichero config de git
vim ~/.gitconfig
Una vez abierto, añadimos en la zona de alias los que queramos. Si no existe la creamos.
Se pueden hacer desde alias sencillos a muy complicados aplicando conocimiento de bash. Un buen alias sería aquel que reseteara los cambios en permisos de ficheros, un error que popdría ocurrir
[alias] st = status reset-permissions = !git diff -p -R --no-color | grep -E \"^(diff|(old|new) mode)\" --color=never | git apply
Notar que negamos el primer git para que no lo copie al ejecutarse puesto que ya lo ponemos nosotros. Sin embargo, hay que ponerlo para que lo reconozca.
Binarios automáticos
Ya hemos visto la utilidad de los alias cuando se trata de comandos complicados.
Los binarios automáticos ayudarán en este sentido cuando el comando sea demasiado largo como para ponerlo en el fichero de configuración.
Solo hay que imaginar cómo quedaría el fichero de configuración con un comando de 5 lineas.
La metodología es sencilla, nos vamos al /bin de nuestro usuairo y editamos un fichero cuyo nombre será el comando que queramos ejecutar.
Eso sí, tiene que empezar por «git-» para que git lo reconozca
cd ~/bin vi git-reset-permissions
Y pegamos el código del punto anterior con la única diferencia que aquí no haría falta negar el primer comando y tampoco escapar las comillas
git diff -p -R --no-color | grep -E "^(diff|(old|new) mode)" --color=never | git apply
Le damos permisos de ejecución
chmod +x git-reset-permissions
Y a partir de ahora ya se podría ejecutar el comando
git reset-permissions
Alias de terminal: mejora la productividad
Hemos visto cómo hacer alias en el archivo de configuración de git. Esto se puede desarrollar al máximo con los alias de terminal.
El ejemplo que habíamos puesto era el de reducir git status a git st.
Para abreviarlo al máximo podemos hacer el siguiente alias de terminal para simplemente usar gs
alias gs="git st"
Trabajo en equipo
Contribuciones: Pull request
Para contribuir a un proyecto hay que hacer una pull resquest dado que normalmente, a menos que el proyecto sea nuestro, no podemos hacer el pull directo.
El paso a paso normal sería el siguiente:
Fork
Así copiamos el proyecto en nuestra cuenta personal de github en el que sí podremos hacer cambios.
Clone
Clonamos nuestro repositorio en local con git clone
git clone git@github.com:user/project.git
Haciendo la contribución
- Hacemos una branch, desarrollamos nuestra contribución y lo ponemos todo en nuestro staging area con un commit y hacemos el push para crear la rama en remoto
- A la hora de hacer la pull request para contribuir se puede hacer directamente desde nuestra rama local al master del origen
- No olvidar de tener seleccionada la opción «allow changes from maintainers»
Flujo de trabajo en equipo
Hay una serie de opciones que encontramos en GitHub que nos pueden ayudar a trabajar en equipos
Labels
No hay que confundir los Labels con los Tags, ya vistos anteriormente y que nos servían para versionar.
El Label se asocia a una issue o a una pull request. Ejemplos de Labels podrían ser: To do, in progress, To review, Accepted, Changes requested
GitHub Project
Es como un Trello que tenemos la opción de utilizar. Por lo general ya se utilizan otros programas como Jira o Redmine
Pull Request Status
Cuando hacemos una pull request podemos hacer un Draft antes de acabar nuestra tarea en lugar de un Create cuando ya la hemos terminado.
En el momento que hacemos un Draft damos la oportunidad a otros de hacer la revisión de lo que estamos haciendo.
Eso nos ayuda a localizar errores antes de que se acabe la tarea y por tanto evita desviaciones de tiempo.
Flujos de Git
Git Flow: Una estrategia con muchas contras
La estrategia parte de 5 tipologías de rama: Develop, Master, Feature Branch, Realease branch y Hotfix branch.
Master
Es la branch principal
Develop
Es la branch que creamos normalmente para trabajar, que puede partir de Master, de Hotfix o de Realease. Se acaban integrando en Master o Realease.
Feature
Branchs que se crean a partir de Develop para añadir nuevas features y que se acaban integrando en Develop
Release
Branch que integra una o varias Develop y que puede subirse a producción sin integrar en Master
Hotfix
Cuando vemos un error en producción y queremos resolverlo rápido y sin entrar en el flujo de otras ramas, utilizamos la Hotfix branch.
Debe partir y acabar en Master pero se pueden crear ramas Develop a partir de estas branchs
En contra de esta estrategia
- De vez en cuando Develop se integra en Master, pero no siempre
- Normalmente Master es igual a Producción, pero en este caso no siempre dado que podría ser una Release
- El histórico con tantas branch quedan muy sucio o ilegible
- El continuous delivery es más complicado
GitHub Flow: la estrategia simplificada
Tan sencilla como tener una branch master e ir haciendo los siguientes pasos:
- New Branch. Creamos la nueva rama a partir de master
- Pull request. Al acabar se hace la pull request de la que obtendremos feedback y haremos los cambios necesarios para poder subirse a producción
- Deploy. A producción se sube la nueva rama de manera que si hubiera que hacer rollback, éste sería muy rápido
- Merge a Master. Cuando se ha asegurado que la nueva branch funciona sin problemas en producción
De esta manera, producción podría ser o master o una branch y se puede entregar valor de forma mmás continua.
GitLab Flow: el foco en los entornos
Con entornos de Producción, Preproducción y un Staging donde se ubica Master y del que saldrán las feature branches
- Es más complejo que el Flow de GitHub
- Mucha confianza al ir a producción
- Aplica con continuous delivery
- Prohibido hacer commits a Master directamente
Release Flow, el flow creado por Microsoft
Como en todos los Flow, de Master parten branches, aquí llamadas Topic, para hacer los desarrollos que se vuelven a integrar en Master.
Al finalizar el sprint, se genera una branch Release de Master, con los nuevos desarrollos, que se sube a producción.
Aumenta el factor riesgo al subir varias tareas de golpe a cambio de menor pérdida de tiempo en cada deploy.
Esto puede tener sentido en algún contexto, pero lógicamente no favorece el continuous delivery
Trunk-Based Development
Es el antiguo Flow de GitHub de lo que solo destacaremos que:
- Puede tener Release branches
- Puede tener tags
- Master es siempre deployable
En entornos como mobile, donde al acabar un desarrollo tenemos que esperar que nos vuelvan a validar la app, funciona muy bien
Master-only Flow o Single Branch Flow
Si se parte siempre de buenas prácticas y se tiene una buena suite de tests se puede trabajar con una sola rama, Master.
Hace falta una alta madurez del equipo.
Se puede integrar a master desarrollos a medias con los llamados Feature Flags. Flags que se activan en base a configuración.
Conclusiones comparativas
Según el tipo de producto y contexto eligiremos uno u otro.
Buenas prácticas
Cómo y qué ignorar
Hay varias maneras de ignorar ficheros:
- A nivel de proyecto, donde estarán las reglas de equipo
- A nivel de entorno, donde estarán nuestras propias reglas, como ignorar ficheros que crean los editores que utilizamos
Para hacerlo a nivel de entorno crearemos o editaremos el fichero .gitignore_global con las reglas y apuntaremos a él desde el fichero de configuración global de git
touch ~/.gitignore_global
Ejemplo para ignorar los ficheros que genera el Mac
.DS_Store
Ejemplo para ignorar los ficheros que genera un editor como IntelliJ IDEA
.idea *.iml *.iws .idea-compl *.sc
Miramos si el archivo ya estaba en la configuración de git
cat ~/.gitconfig| grep ignore
y si no está pues lo añadimos
git config --global core.excludesfile ~/.gitignore_global
Descripciones del commit
Hay ciertas reglas en general que deberíamos seguir, como:
- Hacerlas en inglés para facilitar la lectura a todo programador
- Separar bien el título del contenido o descripción
- El título no puede ser más largo de 50 caracteres
- El cuerpo del commit no más de 72 caracteres por linea
- Primera letra en mayúsculas
- No acabar el commit con un punto
- Utilizar el modo imperativo al escribir
- Utilizar el cuerpo para explicar el «qué» y el «porqué»… el «cómo» es nuestro código
Detalles que nos gustaría encontrar en una pull request
- El cuerpo del commit con no más de 72 caracteres por linea
- Si la PR toca algo de la UI, screenshots
- Si es algo muy complejo, gráficos UML
- Si se necesitan pasos antes del Merge, una checklist
Estrategias de integración entre ramas
Merge
Es una buena práctica mantener nuestras ramas actualizadas para evitar conflictos. Para traer master a la rama donde estamos
git merge master
Rebase
La necesidad de hacer el Rebase surge del Merge. Respeta el histórico de cambios tal cual se han hecho los commits, algo que se puede perder con los Merges.
Por contra, al modificar el histórico para que quede ordenado, puede que hayan cosas que no funcionen en las ramas de compañeros que se creen o descarguen de ya que tienen otro histórico.
No es lo mejor para la resolución de conflictos y las diferentes cronologías entre usuarios podría generarlas.
Squash
El squash se utiliza a través del Rebase y es para juntar varios commits en uno.
En típica situación donde a medida que vamos trabajando una tarea por partes, vamos haciendo commits, esto puede servir para, una vez finalizada la tarea, limpiar un poco el histórico de commits y hacer la entrega final en uno solo.
Para ello utilizaremos el rebase -i señalando el número de commits, por orden cronológico, que queremos que fusione utilizando HEAD~, en este caso 3
git rebase -i HEAD~3
Se nos abrirá un archivo con los commits listados y una explicación de una serie de comandos a seleccionar para cada unos de los commit
pick c24f16d Video 20: Squash 1 pick d6422fc Video 20: Squash 2 pick 1f5d60c Video 20: Squash 3
En el mismo archivo se nos lista los comandos que podemos aplicar en cada uno
Comandos: p, pick = usar commit r, reword = usar commit, pero editar el mensaje de commit e, edit = usar commit, pero parar para un amend s, squash = usar commit, pero fusionarlo en el commit previo f, fixup = como "squash", pero descarta el mensaje del log de este commit x, exec = ejecuta comando ( el resto de la línea) usando un shell b, break = parar aquí (continuar rebase luego con 'git rebase --continue') d, drop = eliminar commit l, label
Empezando con los commit de abajo a arriba, seleccionaremos squash en lugar de pick excepto para el primero, que será el commit utilizado para hacer push.
Si queremos editar el mensaje de ese primer commit, para poner algo en coherencia con lo que se sube, utilizaremos el reword
reword c24f16d Video 20: Squash 1 squash d6422fc Video 20: Squash 2 squash 1f5d60c Video 20: Squash 3
Al finalizar el squash se abrirá el archivo del nuevo commit que podremos editar eliminando todo rastro del resto de commits para que no hagan ruido.
Seguido, podremos hacer un git log para ver cómo ahora solo hay un commit de los 3
Evitar ensuciar el histórico haciendo pull
Se agradece que el histórico de commits sea claro pero esto no siempre es así debido a que Git genera más commits de los que hacemos según el flujo de trabajo que ejecutamos.
Para evitar esto y ver solo los commits reales, cuando nos actualicemos una rama en la que ya hayamos trabajado, lo haremos añadiendo lo siguiente:
git pull origin rama --rebase
o si estás en master simplemente
git pull --rebase
Opciones de integración entre ramas
GitHub nos da 3 opciones. Generalmente seguiremos las convenciones del equipo.
La elección puede estar basada también en cada cuánto commiteamos que depende mucho de la feature que se esté desarrollando.
Merge pull request
Hace un commit de nuestra rama hacia master
Squash and merge
Fusionará todos los commits de nuestra rama y hará un solo commit a master.
De esta manera aparecería en el histórico las features de manera limpia.
El commit final debe ser persistente.
Rebase and merge
Mueve todos los commits y te los pone al principio del histórico.
Al mantener todos los commits respeta el histórico de todo lo que ha pasado.
Convenciones de equipo
Configuración de ramas y merge en GitHub
Explicamos a continuación cómo configurar GitHub para que todo el equipo haga el commit según convención.
Para ello podemos bloquear desde Settings de GitHub las opciones que no queremos que se utilicen.
Entre las opciones de configuración de GitHub, hay otras muy interesantes como las siguientes:
- Require status check before merging: no permite el merge si tenemos algún semáforo en rojo de las aplicaciones que utilicemos tales como Jenkins, así evitamos que puedan haber commits que rompan la build
- Require branches to be up to date before merging: Chequea que la rama esté actualizada con la última versión antes del merge, lo que evita conflictos
- Limitar los usuarios que pueden hacer merge
Una convención habitual es eliminar la rama nada más hacer merge, así evitamos problemas para saber qué ramas están muertas.
Otra es que sea necesario firmar el commit para poderlo hacer. Aquí hay un manual donde explican cómo hacerlo:
https://help.github.com/en/github/authenticating-to-github/signing-commits
Git Hooks
Es un punto intermedio entre pasos de Git. Podemos ejecutar lo que queramos antes de hacer un push o un commit.
Por ejemplo: podemos lanzar test antes de cada push.
Lo más importante es cómo documentarlos para que todos estén informados de los procesos.
Resolución de conflictos
Cómo resolvemos conflictos en Git
Dejando de lado los editores de Github/Gitlab, trabajaremos en nuestro repositorio.
Herramientas como las de Intellij, muy habituales a día de hoy, ayudan a hacerlo más ágilmente.
La mejor estrategia es que cada equipo solo toque una zona de código aislada.
En este punto la arquitectura y el diseño de nuestra aplicación es clave.
Soluciones prácticas
Rollback: Deshaciendo subidas con errores
Revertir cambios en local
Para ver cómo funciona el revert,
tldr git revert
Para deshacer el último commit
git revert @
Si hacemos un git log veremos que el cambio es visible en el histórico con un nuevo commit que revierte el anterior.
Hay otra manera de eliminar el último commit haciendo que desaparezca del histórico
git undo
Este último comando es un alias de git reset HEAD~1 –mixed, que quita el commit del histórico.
Al hacer un git status vemos que los cambios se han quedado en el working tree pero no añadidos en el staging area..
Veamos cómo funciona el git reset:
tldr git reset
El reset puede hacerse soft, hard y mixed. Dependiendo de lo que queramos:
- –soft si queremos mantener los cambios tanto en working tree como en staging area
- –mixed si queremos mantener los cambios en el working tree
- –hard para deshacer los cambios y eliminarlos
También podemos descartar todos los cambios hechos, tanto en el working tree como en el staging area con un alias de reset –hard
git discard
Si de lo que se trata es de añadir unos cambios en el último commit, se puede añadir los cambios en la branch y utilizar el amend
git commit --amend --no-edit
Recordamos que el –no-edit es para que no nos abra la ventana del commit
Si queremos llevar los cambios a master de un commit anterior al último, utilizaremos cherry-pick
tldr git cherry-pick
Nos vamos a master y nos traemos el commit que interesa
git checkout master git cherry-pick $hash_del_commit
Detectando cambios
Cuando se trata de buscar un commit utilizaremos el git log (-i para quitar el case sensitive)
git log -i --grep "texto a buscar"
También podemos filtrar por fechas
git log --since=$(date --date="15 day ago"+"%Y-%m-%d")
Por último podemos saber, para un fichero, los commits y quien los hizo por cada linea con
git blame nombre_fichero
Estas opciones suelen estar integradas en los IDE que visualmente ayudarán mucho más
Localizando cuándo se introdujo un bug
Git viene con una herramienta nativa para localizar bugs: bisect
tldr git bisect
Bisect nos ayudará a hacer una búsqueda binaria entre un conjunto de commits.
Primero miraremos entre qué commits queremos acotar nuestra búsqueda. Utilizaremos git log para saber los hash que nos interesan
git log --oneline
Y seguido ya podremos empezar a buscar del más reciente al último por mirar
git bisect start hash1 hash2
A partir de aquí nos iremos moviendo por commits y tendremos que decir de uno en uno si es bueno o malo
git bisect good git bisect bad
Bisect acabará diciendo cuál es el commit específico con el error.
Una vez lo sabemos, acabamos con la búsqueda
git bisect reset
Podemos automatizar el proceso para saber si el commit es bueno o malo lanzando un test desde consola.
En este ejemplo se ejecutaría un test para saber si el fichero bug existe o no. El test devolverá 0 o uno 1 bajo esa misma lógica
git bisect run "test -f bug"