Apuntes de git

13 de mayo 2019Comentariosherramientas desarrolloDavid Poza SuárezComentarios

Git es un sistema de control de versiones (VCS) lanzado en el año 2005 por el grupo de desarrollo del kernel de Linux, liderado por Linus Torvalds. Tras un cambio en la licencia de Bitkeeper (repositorio privativo), que usaban de forma gratuita desde 2002, tomaron la decisión de crear su propio sistema. Actualmente git es tan usado que prácticamente es el estándar, pues aunque siguen existiendo SVN o Mercurial, fueron superados en porcentaje de uso en 2014.

El que nos ocupa es de tipo distribuido, es decir, la copia local que tiene cada usuario es un repositorio completo, al contrario que hacía SVN o CVS (centralizados), que dependía de un servidor único. Además se basa en instantáneas en lugar de ficheros incrementales, es decir: cuando un fichero cambia, lo almacena entero de nuevo, en lugar de usar solo la diferencia. Justamente por su tipología vienen sus principales ventajas: mayor velocidad y poder trabajar aún estando offline.

Existe el comando git como tal y luego herramientas gráficas para evitarte conocer todos sus parámetros, igual que sucede con otras tantas utilidades. Además hay plataformas para la gestión colaborativa de repositorios en red, como son el archiconocido github(Microsoft), bitbucket(Atlassian) o alternativas self-hosted como gitlab, gitolite, gitosis o gitea.

A estas alturas de la película no voy a descubrir nada a nadie, y menos qué es git. Si eres programador, ya lo conoces, porque si programas en equipo es imposible hacerlo sin un repositorio y si programas solo, también deberías usarlo, te ayuda a ser organizado e ir sobre seguro: porque es muy muy difícil perder trabajo si lo usas y puedes hacer cambios con la tranquilidad de poder volver atrás en cualquier momento o de mantener aisladas las nuevas implementaciones de la versión estable.

No obstante, para mí, este artículo tiene todo el sentido del mundo: me ayuda a sintetizar ideas y a hacer un resumen de los comandos más importantes, por que sí, me gusta conocer los comandos que posee aunque existan herramientas de tipo GUI, como el mismo Visual Studio Code, Atom o SourceTree por poner un ejemplo, que nos hacen la vida más fácil pero a veces pueden generar dudas sobre qué estás haciendo exactamente. Así cuando olvide algo tendré mis apuntes/cheatsheet aquí disponibles.

Conceptos/nomenclatura

estructura de un commit

estructura de un commit

  • repositorio: Es el espacio donde se almacenan todos los ficheros del proyecto, con un historial de todos los cambios que han ido sufriendo a lo largo del tiempo.

    • local: Es un repositorio completo que tiene cada usuario en su máquina.
    • remoto: Es una referencia a cualquier otro repositorio que no sea el local. Para colaborar con otros usuarios se usan repositorios remotos, que van sincronizando y mezclando (merge) con el contenido de sus repositorios locales, surgiendo en ese momento los conflictos.
  • commit: Conjunto de cambios (changeset) sobre uno o varios ficheros del proyecto, que lleva asociado un breve mensaje descriptivo y un autor. Y que se identifican de forma única con una cadena del tipo "eda792...8bed6c" (41 caracteres), que no es más que un [hash de tipo SHA-1](https://es.wikipedia.org/wiki/SecureHashAlgorithm#SHA-1) de la fecha, autor, mensaje, id del snapshot(árbol) en ese momento y el id del anterior commit (un commit guarda un enlace a su anterior commit o a varios commit si se trata de un merge de dos o más ramas). Como el id es un string muy largo, habitualmente se hace referencia al mismo usando solo los 7 primeros caracteres.

    • tree/snapshot: es una estructura de árbol que representa el estado de todos los ficheros del proyecto en un momento concreto en el tiempo. Cada commit guarda la referencia a un snapshot.
  • rama: Una rama es una bifurcación en la línea de desarrollo del proyecto, que tendrá su propio historial de commit y estará separada por completo de la rama por defecto. Se usan para implementar nuevas funcionalidades sin afectar a la versión estable del proyecto. Técnicamente una rama es un puntero al último commit de una línea de desarrollo. Cuando creamos una rama, simplemente estamos creando un nuevo puntero que podemos mover libremente respecto al resto de ramas.

    • rama master: Es la rama creada por defecto.
  • conflicto: Sucede cuando se unen dos ramas en las cuales se modifican los mismos trozos de un mismo fichero y git no sabe cuál de las dos elegir, por lo que somos nosotros quienes debemos tomar la decisión para resolverlo.
  • HEAD: También es un puntero, que apunta al último commit de la rama en la que te encuentres trabajando actualmente, salvo que hayamos movido HEAD a propósito a un momento en el pasado.
  • tag: Es un nombre con un puntero a un momento concreto en la historia del desarrollo. Se suelen usar para identificar los momentos en que se lanzan nuevas versiones.

Ahora sí, vamos a comenzar por lo más simple:

Creación y configuración

  • git config --global user.name "David Poza Suárez": Establece el nombre de usuario que va a figurar como autor del commit, que podremos ver en los logs.
  • git config --global user.email "hello@davidinformatico.com": Junto con el nombre aparece en el campo author de cada commit.
  • git config --global --list: Para visualizar nuestra configuración actual. Debemos saber que toda la configuración se guarda en el fichero .gitconfig, que se encuentra en el HOME de usuario en Linux o en C:\Users si estas en Windows.
  • git init: Crea un repositorio local vacío en el directorio actual. Este se almacena en el directorio .git.
  • git clone <url>: Descarga una local copia de la rama master (únicamente) de un repositorio. Además configura un repositorio remoto con nombre origin asociándole la url desde la que hemos hecho clone. De este modo ya podríamos hacer pull sin tener que añadirlo manualmente.
  • git fetch [repositorio remoto]: Descarga los cambios (por ejemplo ramas nuevas) que se hayan producido en el repositorio remoto. Si se omite el repositorio, por defecto usa origin.

    • git fetch --prune: Descarga los cambios y además borra las ramas que ya no existen en el remoto.
  • git status: Este es el más usado de todos. Nos dice la rama en la que nos encontramos y los ficheros que contiene el stage junto a su estado (new/modified/deleted). Además avisa de ficheros sin seguimiento por git (untracked) o ficheros con conflictos.

Para ver la ayuda de un comando en concreto podemos usar el flag h. Por ejemplo: git clone -h

Áreas o zonas

estructura git 1024x960

  • Working dir/Área de trabajo: Es el estado actual de nuestros ficheros, aquellos en los que programamos y que podemos visualizar en nuestro navegador de archivos.
  • Staging Area/Index/Área de ensayo: Es una zona intermedia donde vamos almacenando los cambios que van a conformar un commit. Pueden ser ficheros completos o solo porciones concretas. Para "pasar" ficheros a esta zona usamos el comando add. Necesariamente hay que enviar los cambios al staging area antes de poder realizar un commit.
  • El repositorio local: Es donde se almacenan los commits mediante el comando commit, una vez hemos seleccionado los cambios en el index. Toda su información se guarda en el directorio .git.
  • Los repositorios remotos: Puede ser uno solo (por defecto se llama origin), o podríamos tener varios. La comunicación entre el repositorio local y los remotos se realiza mediante los comandos push(enviar) y pull(descargar).
  • *Stash: Es la zona de guardado rápido, una zona auxiliar para guardar los cambios en los que estamos trabajando cuando por algún motivo nos interrumpen y tenemos que cambiar de rama, pero aún no queremos hacer un commit porque es un commit a medias, sin acabar. Puede almacenar n estados y funciona como una pila, colocando siempre el primero los últimos cambios que salvemos.

No es posible hacer un push a un remoto de un commit que se encuentre en la zona de stash. Por lo que esta zona es únicamente de uso privado.

Flujo entre stash/working/stage/repo

  • git add <lista de ficheros>: Manda uno o varios ficheros separados por espacios o un directorio al staging area. Recién añadido un nuevo fichero al repositorio, este se encuentra en estado "untracked" y debemos hacer un primer add. Podríamos usar el carácter "." si queremos enviar todos los ficheros del directorio de trabajo que tienen algún cambio. También admite comodines, ej: git add *.js

    • git add --patch: Como he comentado justo arriba, podríamos preparar un commit con una porción concreta de un fichero y no todos los cambios que tiene (este es el típico comando que puede ser más interactivo con un GUI), para lo cual habría que enviar al Index ese fragmento y no valdría con un simple git add fichero. Lanzar el comando con esta opción lanza un modo interactivo en el que vamos saltando de trozo en trozo y se nos pregunta entre las siguientes opciones:

      • y: marca el trozo para añadirlo al stash
      • n: marca el trozo para no enviarlo al stash
      • q: cerrar modo interactivo sin enviar el trozo actual ni los siguientes (pero sí los que ya hubiesen sido marcados).
      • a: enviar el trozo actual y todos los posteriores al stash
      • d: cerrar modo interactivo, no enviar ningún trozo, abortar la operación add.
      • k,j: para ir al siguiente o al anterior trozo aún sin marcar.
    • git reset HEAD fichero / git rm --cached fichero: Con estos comandos podemos sacar un fichero de la zona Index. *NOTA: El primer comando solo es válido si ya existe al menos un commit en el repositorio, caso en el que usaríamos el segundo.
    • git mv fichero.txt renombrado.txt: Si queremos modificar el nombre de un fichero. Si hacemos un git status nos aparecerá como renamed.
  • git rm <lista de ficheros>: Es igual que add, pero para cuando hemos borrado un fichero. Es decir, envia un fichero borrado al Index.
  • git commit -m "descripción breve": Registra definitivamente los cambios que previamente estaban en el Stage. Un commit contiene:

    • id: Identifica el cambio de forma única. Es el hash SHA-1 del resto de campos.
    • mensaje: que habitualmente se trunca a 72 caracteres al visualizarlo en el log o github.
    • autor: El autor del cambio identificado por su name y su email.
    • id del árbol o snapshot en ese momento.
    • id del anterior commit.
  • git commit -am "descripción": Hace un commit con todos los ficheros con cambios. Por tanto esta opción a es para realizar el add previo y el commit de una sola vez. *Importante: solo funciona si el fichero ya está en seguimiento por git (tracked).
  • git stash: Envía todos los ficheros que tengamos en la zona de trabajo y en el Index(ambas), excepto los untracked a la zona Stash. Se guardarán con un identificador del tipo: stash@{0} y un mensaje. que por defecto será el mismo que el último commit que tengamos en el repositorio.

    • git stash push -m "descripcion": Permite especificar un mensaje para el commit que estamos enviando al stash.
  • git stash pop: Devuelve el último commit de stash y lo elimina de dicha zona, pasándolo a la zona de working space o Index, según donde lo tuviésemos originalmente.
  • git stash apply <id>: Recupera el commit del stash con el identificador especificado, pero lo mantiene en el stash.
  • git stash drop <id>: Borra un commit concreto del stash.
  • git stash clear: Borrar el stash al completo.
  • git stash list: Muestra los commits que tengamos en la zona de stash.

Fichero .gitignore

Este fichero que se encuentra en la raíz del working directory y sirve para hacer que git ignore la existencia de ciertos ficheros o directorios, de forma que no realice ningún seguimiento de estos sin reportarlo en el git status. La idea es incluir aquí los ficheros autogenerados, temporales, compilados etc, que se obtienen a partir de los fuentes. Admite diferentes comodines y ciertas expresiones:

  • comentarios: líneas precedidas por #
  • *.tmp: Podemos usar el comodín para indicar cero o más caracteres. Ej. fichero.tmp, fichero\prueba.tmp, log.tmp_
  • fichero?.tmp: La interrogación indica exactamente un carácter. Ej. fichero1.tmp, fichero2.tmp
  • **/fichero.txt: EL doble asterisco sustituye a cero o n directorios. Ej: src/components/component1/fichero.txt, src/api/index.js, /fichero.txt
  • fichero_[a-z].tmp: podemos usar conjuntos de caracteres definidos por rangos. Ej: fichero\a.tmp, fichero_b.tmp_
  • fichero_[0123].tmp: podemos usar conjuntos específicos. Ej: fichero\1.tmp, fichero_2.tmp y fichero_3.tmp_
  • !fichero.tmp: Podemos incluir excepciones a ficheros ignorados en las líneas previas.
  • directorio/: Debe incluirse la barra final para ignorar todo el directorio y su contenido. Si no incluimos la barra entonces ignora directorios con ese nombre pero también ficheros con el mismo. Ej: directorio/fichero.txt, directorio/subdir/fichero.txt, directorio/subdir2/fichero.txt
  • directorio/subdir/fichero.txt: un fichero en una ruta concreta.

Se puede tener un .gitignore global para todos nuestros repositorios, situándolo en el directorio de usuario.

Referencias relativas

Cuando queremos hacer una referencia a un commit concreto (ej. en comandos show, checkout...), podemos hacerlo usando su id, pero también podríamos hacerlo usando referencias relativas a una rama o al puntero HEAD:

  • git show <commit_id>
  • git show <rama>: Nos muestra el último commit de la rama indicada.
  • git show <HEAD>: Nos muestra el commit apuntado por HEAD, por defecto el último de la rama activa.

Virgulilla y acento circunflejo

Podemos referenciar un commit anterior (padre) a partir de un puntero de rama o de HEAD:

git referencias relativas

  • git show master^: Con el acento circunflejo podemos ver el commit anterior al que apunta master.
  • git show HEAD^^^: Vemos el tercero empezando por el final.
  • git show rama_x^^: Vemos el commit antepenúltimo apuntado de master.
  • git show rama_y~2: La virgulilla nos permite ir N commits atrás.
  • git show HEAD~5: Vemos el quinto empezando por el último.
  • git show HEAD^1: Significa el padre del commit apuntado por HEAD.
  • git show HEAD^2: OJO!. No es lo mismo que HEAD~2, significa el segundo padre del commit apuntado por HEAD. Recordemos que en un commit de merge tiene más de un padre.
  • git show HEAD~2^1: Se puede combinar el uso de acento circunflejo y virgulilla. Este caso equivale a HEAD~3.

Podemos ver un listado de commits con sus referencias relativas usando el comando "git show-branch --more=<número de commits>":

Recuperar/deshacer cambios

  • git commit --amend: Se modifica el último commit con el contenido que tengamos en el Index, muy útil cuando se nos ha olvidado añadir algo en el commit o nos hemos confundido en la selección de cambios para incluir en el commit. *Realmente borra el commit y crea otro de nuevo.

    • git commit --amend -m "corrección del mensaje": También podría ocurrir que nos hayamos equivocado en el mensaje del commit.
  • git checkout -- <fichero>: Deshacer cambios (o recuperar un fichero borrado) que solo están en el área de trabajo. *Si además hemos hecho add de los cambios, debemos hacer primero el comando reset HEAD del fichero: git reset HEAD fichero para sacarlo del stage y posteriormente recuperarlo con git checkout -- fichero.
  • git checkout <commit-id> <fichero>: Devuelve un únicamente fichero concreto al estado que tenía en el commit indicado. *Pero el puntero HEAD no se mueve.
  • git revert <ref>: Nos permite crear un commit que invierte los cambios de la referencia indicada. Es decir, si indicamos un commit que añadía una línea y borraba dos, ahora estamos creando otro commit que borra la línea añadida y añade las dos borradas.
  • git revert -m 1 <ref>: Para poder revertir un commit que es un merge. Con el parámetro -m 1 le estámos diciendo que volvemos al padre del commit en la rama base (master), mientras que con el parámetro -m 2 volveríamos al commit de la rama que se estaba integrando (dev) en dicho merge.

image

  • git revert -m 1 <ref> -n: Con la opción '-n' o '--no--commit' evitaremos que se cree automáticamente el commit, dejando el revert en el stage. Esto es muy útil si no queremos hacer el revert completo del commit, sino que queremos descartar algunos ficheros 🤏.
  • git reset: Elimina commits. Técnicamente lo que hace es modificar el puntero de la rama activa al valor que le indiquemos.

    • git reset --mixed <ref>: (Opción default si no se especifica nada): Elimina los commit del repositorio local que queden por detrás de la nueva posición del puntero pero todos los changeset son guardados/combinados en el área de trabajo además de los cambios que ya tuviera dicho área.
    • git reset --soft <ref>: Elimina los commit que queden por detrás de la nueva posición del puntero pero todos los changeset son guardados "combinados/squashed" en el Index/Stage. Borrar n commits en modo soft y seguidamente un commit se suele llamar hacer "squashing", ya que es como aplastar o compactar n commits en uno. NOTA: En caso de compactar varios commits que modifican la misma línea de un fichero, nos estaríamos quedando finalmente con el contenido que tuviese en el commit más reciente en el tiempo.
    • git reset --hard <ref>: CUIDADO! Todos los commit que queden por detrás de la nueva posición del puntero son borrados para siempre.
    • Ejemplo: git reset --soft master~2 borra los dos últimos commit.
    • Otro ejemplo: si queremos volver la rama tal y como está en el remoto:git fetch origin y a continuación git reset --hard origin/master
    • Y por último, en caso de querer deshacer el primer commit: git update-ref -d HEAD
  • git reflog (equivalente a git reflog show HEAD): ¿Qué pasa si queremos rehacer un cambio que hemos revertido con git reset? ¿Hemos perdido la posición de HEAD para siempre? La respuesta es NO. el comando reflog guarda un histórico de los últimos valores que ha ido tomando el puntero HEAD de la rama, y podemos rehacer un reset hacía atrás simplemente haciendo otro reset hacia el commit posterior que consultemos en el reflog. OJO!: reflog no guarda infinitos estados: hasta no tener 7000 objetos fuera sin empaquetar o más de 50 ficheros empaquetadores no se lanza el recolector de basura (git gc, que puede ser lanzado a propósito). NOTA: Para hacer referencia a un objeto de reflog podemos usar su id o también la sintaxis HEAD@{1} indicando la posición en la pila de cambios, siendo 0 el estado actual.

    • git reflog show --all: Para ver el log de todos los objetos del repositorio completo.

IMPORTANTE: Como buena práctica, nunca deberíamos modificar ni eliminar commits que ya han sido subidos a un remoto, podemos liarla bien gorda.

Histórico de cambios

Log

Otro de los comandos más usados es git log. Abre un listado que nos va a permitir ver el histórico completo de todos los commit que se han ido realizando en el repositorio a partir de HEAD hacia atrás, incluyendo los que nos traemos del remoto cuando hacemos un fetch. Funciona en modo interactivo y por defecto los últimos cambios aparecen los primeros.

Podemos desplazarnos con las _flechas arriba/abajo_ o bien usando la tecla _space_ para saltar de pagina en pagina. También podemos buscar dentro de cualquier contenido del commit usando la barra: /palabra y enter. Para salir del modo interactivo, como es habitual en linux, hay que pulsar la tecla q y luego enter.

Podemos desplazarnos con las flechas arriba/abajo o bien usando la tecla space para saltar de pagina en pagina. También podemos buscar dentro de cualquier contenido del commit usando la barra: /palabra y enter. Para salir del modo interactivo, como es habitual en linux, hay que pulsar la tecla q y luego enter.

Opciones de log

  • git log <commit-id|rama|HEAD>: Muestra el log a partir del commit, rama o puntero HEAD.
  • git log -3: Muestra los últimos n commits.
  • git log HEAD~20..HEAD~25: Con los dos puntos muestra un rango concreto de commits.
  • git log --since={YYYY-MM-DD HH:MM:SS}: Muestra los commits desde la fecha dada.
  • git log --before={YYYY-MM-DD HH:MM:SS}: Muestra los commits antes de la fecha.
  • git log --after={YYYY-MM-DD H:MM:SS}: Muestra los commits después de la fecha dada.
  • git log -- <fichero>: Muestra todos los commits en los que se ha modificado el fichero indicado.
  • git log --stat: Muestra un resumen del total de ficheros modificados y de las líneas añadidas y borradas de cada uno.
  • git log --grep='regex': Podemos hacer una filtro de commits usando una expresión regular.
  • git log --no-merges: Para no mostrar los merges en el listado de commits.

    git log stats

  • git log --oneline: nos permite ver un commit por linea, mostrando solo el id y el mensaje.
  • git log --pretty=format:"%h(%an %ar): %s": Permite personalizar el formato del log usando variables predefinidas o placeholders. En el ejemplo obtendríamos:

custom git log 1

salida de log personalizada

  • git log --oneline --decorate --graph: Con la opción graph podemos visualizar de forma "gráfica" las ramas y sus merges en el tiempo.

git graph

Viajando en el tiempo

  • git checkout <commit_id|rama|HEAD>: Podemos usar checkout para visualizar el estado del proyecto en un commit concreto, es decir, en ese momento en el tiempo. Con esta operación lo que internamente estamos haciendo es apuntar a ese commit con el puntero HEAD.

Inspeccionar un commit:

  • git show <commit_id>: Nos permite visualizar los cambios en el código que incluye un commit concreto.

Encontrar diferencias:

  • git diff: Permite ver las diferencias en el código entre el working directory y el Index.
  • git diff --cached: Permite ver las diferencias en el código entre Index y el último commit. git diff HEAD: Permite ver las diferencias en el código entre el working directory y el último commit.
  • git diff <commit1_id> <commit2_id> -- [fichero]: Permite ver las diferencias en el código de un fichero concreto (si no especificamos entonces compara todos) y su estado en el commit1 y el commit2.
  • git diff --stat rama1..rama2: Para visualizar un listado de ficheros que han cambiado entre ramas.
  • git diff --stat rama1 rama2: Lo mismo que el caso anterior pero para el caso de que los nombres de rama contengan guiones u otros caracteres que provoquen confusión en la ejecución del comando.

Por defecto diff usa un tamaño de contexto de 3, es decir: muestra 3 líneas antes del cambio y otras 3 después del mismo. Pero si necesitamos un contexto mayor para situarnos mejor en el código entonces usaremos el parámetro -U, por ejemplo git diff -U10 usará un tamaño de contexto de 10 líneas.

Explicación de la salida de git diff

Explicación de la salida de git diff

Trabajar con ramas

  • git branch: Nos muestra la rama activa.
  • git branch <nuevarama>: Crea una nueva rama a partir del commit apuntado por HEAD.
  • git branch <nuevarama> <commit_id>: Crea una nueva rama a partir del commit indicado.
  • git branch -d <rama>: Borra la rama.
  • git push <remoto> <rama-local>:<rama-remota>: Si hacemos un push al remoto con el nombre de la rama remota precedido del símbolo ":", se borrará la rama remota.
  • git branch --list: Listado de las ramas locales.
  • git branch -a: Lista de ramas tanto locales como remotas.
  • git branch <rama-local> <remoto/rama-remota>: Clonar una rama en concreto. Recordemos que el comando clone solo trae la rama master. NOTA: No nos posiciona en la rama clonada.
  • git checkout <rama>: Cambia la rama activa a la indicada.

    • git checkout -b <rama>: Hace dos pasos en uno: crea la rama a partir de la activa y se cambia a ella.
    • git checkout -b <rama-local> <remoto/rama-remota>: Clona la rama en concreto pero además se posiciona en ella.
  • git merge <rama>: Se fusiona la rama indicada con la rama activa en ese momento, creando un commit de merge, que tiene dos commit padres, que podremos ver en el log.

    • git merge <rama> --squash: Se fusiona la rama indicada en la rama activa, pero sus n commits se juntan en uno solo, de forma que en la historia de la rama activa solo figurará un commit de mergeo.
  • git rebase <rama>: Se copian los commits de la rama indicada a la rama activa alterando el historial, y además no se genera un commit de merge extra. Por lo tanto el log quedará totalmente plano, sin ver de donde vienen los commits que mezclan dos ramas.

    • git rebase --continue: Si en el comando anterior se produce un conflicto al copiar un commit, el proceso se para y debemos resolverlo. Una vez resuelto, debemos lanzar un git add . y podemos continuar con el rebase con el flag continue.
    • git rebase --abort: Si en mitad de un rebase queremos parar el proceso podemos usar esta opción.

Podemos usar rebase para borrar un commit en mitad de la historia de la rama.

git rebase -p --onto . Ej: Tenemos esta historia de commits: A--B--C--D--E master y queremos borrar el commit C. Hacemos: git rebase --onto B C, o git rebase --onto B D^, o también git rebase --onto C^ C Con esto estamos haciendo que B sea la nueva base de D.

  • git cherry-pick <commit-id>: Aplica el commit indicado en la rama activa, generando un commit nuevo. Es útil por si no queremos traer todos los commits con un rebase.

    • git cherry-pick <commit-id> -e: Permite editar el mensaje que va a figurar en el nuevo commit en lugar de limitarse a copiarlo del commit indicado.
    • git cherry-pick <commit-id> -n: Trae el commit al working area en lugar de aplicarlo automáticamente.

La ventaja de rebase es que deja un log mucho más claro, como si el trabajo se hubiese hecho en serie y no en paralelo, sin embargo es un comando destructivo que no puede deshacerse. Se usa sobre todo para mantener la rama feature actualizada respecto de la master. Una buena práctica es hacer un rebase al comienzo del día y otro al final, para no desfasarse mucho respecto de la rama master.

diferencias entre merge y rebase

diferencias entre merge y rebase

Diferencias entre merge, squash y rebase

Detached HEAD y el no-branch/el limbo

Cuando nos movemos con el comando checkout a un commit anterior, (estamos moviendo el puntero HEAD y desligandolo de la rama actual) y comenzamos a hacer más commits, estaremos creando una nueva línea temporal, pero sin un puntero de rama que nos permita seguirlos. Por ello éstos van a quedar "en el limbo" porque no van a poder recuperarse en cuanto cambiemos de rama.

IMPORTANTE: Siempre que movamos HEAD a un punto en el pasado para añadir cambios a partir de ahí, debemos crear una rama, para poder volver más tarde a esa nueva línea de desarrollo.

Repositorios remotos

  • git remote add <alias_repo> <url>: Nos permite añadir un repositorio remoto y asociarlo con un alias. Como decía al principio del artículo, git clone automáticamente crea el alias origin apuntando a la url desde la que clonamos.
  • git remote -v: Lista todos los alias a repositorios remotos que tenemos dados de alta en nuestro repositorio local.
  • git remote rename <nombre_antiguo> <nombre_nuevo>: Modifica el alias de un repositorio remoto.
  • git remote remove <remoto>: Elimina el alias del remoto indicado.
  • git push <repo-remoto> <rama>: Envía todos los commits de la rama indicada que no existan en el repositorio remoto.
  • git pull <repo>: Es el equivalente a realizar un fetch + merge (lo veremos más adelante). Si no indicamos repositorio, por defecto usa el repositorio origin.

Resolviendo conflictos

Cuando hemos recuperado un fichero de un remoto(pull) o bien de la zona stash(pop/apply), y este contiene modificaciones en las mismas líneas que también hemos modificado en la zona de working directory, se produce un conflicto que git no puede resolver automáticamente haciendo un automerge o fusión automática. Somos nosotros los que debemos decidir qué contenido queremos que tenga la línea o líneas finalmente.

Podemos saber en qué ficheros hay conflictos lanzando un git status.

Podemos saber en qué ficheros hay conflictos lanzando un git status.

Si visualizamos un fichero con un conflicto sin una herramienta para git, simplemente veremos bloques separados de esta forma. Podemos solucionarlo editando el fichero y dejando solo la línea que queramos, quitando las líneas <<<<<<, >>>>>> y =======

Si visualizamos un fichero con un conflicto sin una herramienta para git, simplemente veremos bloques separados de esta forma. Podemos solucionarlo editando el fichero y dejando solo la línea que queramos, quitando las líneas <<<<<<, >>>>>> y =======

Si usamos una herramienta como VStudioCode, es aún más intuitivo, pero es exactamente lo mismo.

Si usamos una herramienta como VStudioCode, es aún más intuitivo, pero es exactamente lo mismo.

Una vez hemos editado el fichero y solucionado el conflicto, debemos informar a git, para lo cual debemos hacer un git add prueba.txt y seguidamente un git commit -m "conflicto resuelto".

Flujo de trabajo

El gitflow más habitual consta de los siguientes puntos para organizarse:

gitflow

  • Se deja la rama master como rama estable o de producción. En ella vamos etiquetando las diferentes versiones, para poder identificar el momento exacto en el tiempo en que liberamos cada versión.
  • Se crea una rama develop donde iremos trabajando para conformar la siguiente versión estable. Cuando todos los test son correctos podemos fusionarla con master.
  • Además de estas dos ramas, se crean ramas temporales o ramas feature/topic, que son únicamente para desarrollar una funcionalidad concreta y una vez sean fusionadas con develop serán borradas.
  • Si aparece un bug, se crea una rama hotfix a partir de master para resolverlo. Una vez se ha completado la solución al bug debemos de fusionar la rama hotfix con la develop y además, si el fallo es grave, se mergea también con master, en lugar de esperar a completar la siguiente versión que estemos trabajando en develop para liberar el hotfix.
  • También se pueden crear ramas para las diferentes releases, a partir de la rama develop, como antes de hacer merge con master. Una vez finalizada la release, debe incorporarse tanto a develop como a master.

Una buena costumbre es hacer siempre un push a un remoto al finalizar la jornada laboral, es decir, debemos acostumbrarnos a dejar limpia el área de trabajo. Sin embargo es posible que no hayamos completado ninguna funcionalidad y el commit que hagamos sea un commit sin mucho sentido. Por lo que una buena solución es tener un repositorio remoto separado del repositorio oficial del proyecto, donde hagamos esos commit temporales, para asi no ensuciar el repositorio del proyecto.

Flujo de trabajo centralizado

Este modelo de organización es el más básico posible e implica que todos los contribuidores al proyecto tienen permisos en el repositorio y pueden hacer push o borrar ramas en el proyecto. Este modo de trabajo tiene ciertos riesgos ya que cualquiera de los usuarios puede cometer errores muy graves, y requiere tener mucha confianza en los contribuidores.

flujo centralizado 1024x841

Flujo de trabajo basado en fork

Esta es el flujo de trabajo habitual en proyectos de código libre. Tan solo hay unos pocos contribuidores de confianza con permisos sobre el proyecto, el resto son colaboradores sin permisos, que se clonarán el repositorio en su usuario de github por ejemplo (crean un fork). Teniendo así control total de él, donde trabajarán como en el workflow centralizado: se bajarán su copia local y comenzarán a crear ramas y a hacer commits, para ir haciendo push a nuestro remoto origin.

El contribuidor tendrá por tanto en su fork, dos remotos:

  • El suyo donde podrá hacer push para salvar el trabajo (origin)
  • El oficial del proyecto, donde no tiene permisos, y que por convención llamaremos upstream. Que iremos actualizando con pull (o mejor rebase) cada vez que acabemos una rama de feature o cada vez que consideremos oportuno para no quedarnos demasiado desfasados del proyecto.

Una vez han fusionado los cambios con el master de su fork deben crear un pull-request (así se llama en github, pero el gitlab por ejemplo se llama merge-request) contra el repositorio oficial del proyecto. En el momento que el propietario/integrador del repositorio acepte el pull-request se generará la opción de realizar un merge, un merge --squash o un rebase (ver diferencias) que también debe ser aceptado para que finalmente la contribución sea añadida.

github select merge rebase

al aceptar un pull-request podemos integrar la rama mediante un merge, un merge --squash o un rebase

Si surgieran conflictos en el merge podría ser el integrador quien los resuelva. O bien el contribuidor podría descargar la última versión de upstream/master y hacer el merge en local para resolver los conflictos y posteriormente hacer el pull-request con garantía de que no aparecerán conflictos.

fork flow

Basura

Git trabaja guardando todo en objetos, que no son más que ficheros con un hash asociado. Estos objetos pueden ser: commits, árboles, blobs o tags. Se almacenan en el directorio .git/objects. Además existe el concepto de paquete, que es un archivo comprimido que contiene varios objetos, y se usa para ganar eficiencia en el almacenamiento.

  • git count-objects -vH: Podemos consultar el número de objetos que tiene el repositorio con este comando . Su salida sería algo parecido a esto:
count: 513
size: 826.17 KiB
in-pack: 2196
packs: 1
size-pack: 1.16 MiB
prune-packable: 0
garbage: 0
size-garbage: 0 bytes

Todo el mantenimiento de los objetos y paquetes lo hace el comando recolector de basura:

  • git gc: Fuerza el empaquetado de los objetos sueltos y junta todos los ficheros de referencia de cada rama en uno solo. De hecho cuando hacemos un clone de un repositorio ya está limpito y trae todo empaquetado.

El reflog no se salva al hacer un push, ya que contiene un log diferente en cada usuario local, por lo que no tendría sentido subirlo al repo. Sin embargo el reflog se almacena en el directorio .git/logs, que podemos salvar si lo creemos oportuno. Al fin y al cabo todos los objetos, aunque empaquetados, sí están respaldados en el remoto.

Un problema que puede surgir es que en algún momento se añada algún fichero binario grande al repositorio y esté ocupando durante mucho tiempo a pesar de haberlo borrado porque se mantiene en el reflog.

  • git reflog expire --expire=now [--all|ref] && git gc --prune=now --aggressive: IMPORTANTE!!!!!: Esto es destrucción total, primero borra el reflog y luego BORRA LOS OBJETOS QUE NO FIGURAN EN EL REFLOG DEL DISCO DURO, es decir, eliminará cualquier posibilidad de recuperar esos objetos que ahora no se están referenciando en ningun lado.

Documentación

Como siempre digo, hay que tener a mano la documentación oficial, que en este caso es magnifica.