git worktree

Porque me puede el ansia y quiero trabajar a la vez en varias cosas sin que se pisen entre ellas. Pero claro, copiar el directorio o clonar varias veces el mismo repositorio no parece la mejor opción.

Por eso, aparece al rescate git-worktree.

git worktree no es algo nuevo, lleva en git (de una forma u otra) desde julio del 2015, pero es ahora con la IA cuando ha ganado popularidad y es una herramienta must-have.

Primero ¿qué es y para qué sirve un worktree?

Los worktrees permiten trabajar en varias ramas de un repositorio a la vez, cada una en su propio directorio. Es un copiar-pegar pero gestionado por git.

Y como decía en la entradilla del post, para trabajar en varias ramas de forma simultánea sin los worktrees te quedaba bien copiar el repositorio en distintos directorios o bien clonarlo de nuevo en una ubicación distinta. Ninguna de las dos es una buena idea.

Para verlo funcionar con un ejemplo, preparemos un directorio en el que podamos trabajar:

mkdir worktrees-playground
cd .\worktrees-playground\
mkdir my-app
cd .\my-app\
echo Foo > foo.txt
echo Bar > bar.txt
git init
git add .
git commit -m "Initial commit"

Ten en cuenta que cada nuevo worktree vive en su propio directorio, que puede estar dentro o fuera del worktree principal. Además, el directorio que hemos creado hasta ahora (en nuestro caso my-app/) ya es un worktree. Es decir, todo es un worktree. Puede ser main (el que conocemos hasta ahora) o linked, pero worktree al fin y al cabo.

Esta convención de main worktree y linked worktree no es inventada, es como se nombra en la documentación oficial de git.

Por eso si ejecutamos git worktree list dentro de my-app/ nos devuelve un resultado, que es el propio my-app/.

En cuanto a dónde ubicar los nuevos worktrees (los linked worktrees), veamos cómo lo hacen los que saben:

  • Visual Studio Code (fuera, como directorio hermano): <repository>.worktrees\<name>
  • Claude Code (dentro del repositorio): <repository>\.claude\worktrees\<name>

En mi opinión, que Claude Code los meta dentro no es una buena idea y obliga a añadir .claude/worktrees/ a .gitignore.

Para crear un worktree (y poder ya verlo con nuestros ojos): git worktree add <path> -b <new-branch> <commit-ish>. Por ejemplo: git worktree add ..\my-app.worktrees\feature-1 -b feature/1 main

Si queremos simplificar este comando, podemos usar git worktree add <path> y entonces el nombre de la rama se infiere de la última ruta del path y commit-ish es HEAD, que es donde esté apuntando actualmente la rama activa del main worktree.

Ahora, git worktree list nos devuelve dos resultados:

C:/worktrees-playground/my-app                      0a29c7a [main]
C:/worktrees-playground/my-app.worktrees/feature-1  0a29c7a [feature/1]

Además, si has configurado tu prompt como te decía aquí deberías ver de un vistazo si estás en un linked worktree con el icono .

Eso sí, no intentes tener la misma rama activa en más de un worktree, no se puede y Git se quejará de ello como un fatal: 'feature/1' is already used by worktree at 'C:/worktrees-playground/my-app.worktrees/feature-1'.

A partir de aquí puedes trabajar en ambos directorios con total normalidad: nada se va a pisar. Cada uno es independiente y tiene su propio working directory y su propio index, así que puedes editar ficheros y crear commits en los dos sin interferencias. Es lo mismo que trabajar con ramas de siempre, solo que en lugar de ir saltando de una a otra las tienes todas disponibles a la vez.

Cabe mencionar que el linked worktree no tiene directorio .git/. Es el main worktree quien tiene en .git/worktrees/<name> un fichero gitdir que apunta al linked worktree.

Sin embargo (y antes de meternos con el merge) vamos a ver cómo deshacernos de los worktrees (porque no, no deberías borrar los directorios sin más):

# Elimina el directorio del worktree y su vínculo con el repositorio
git worktree remove ..\my-app.worktrees\feature-1
# Con --force lo elimina aunque haya ficheros sin seguimiento o cambios sin confirmar
git worktree remove --force ..\my-app.worktrees\feature-1

# Eliminar el worktree no borra la rama, hay que hacerlo aparte
git branch -d feature/1
# -D borra la rama aunque tenga commits sin fusionar
git branch -D feature/1

Y si no pudiste evitarlo y eliminaste el directorio manualmente:

# Elimina vínculos de worktrees que ya no existen
git worktree prune 

También puedes mover un worktree de sitio con git worktree move <worktree> <new-path>. Por ejemplo, git worktree move feature-1 C:/worktrees-playground/my-app.worktrees/feature-1-moved. Esto cambia el directorio, pero no la rama: el worktree sigue teniendo activa la misma que antes. Puedes renombrar la rama con git branch -m feature/1 feature/2.

Si moviste el directorio del worktree a mano (que tampoco deberías) puedes reparar el vínculo con git worktree repair <path-moved>.

En cualquier caso, como lo que queremos es trabajar en paralelo sin pisarnos (es decir, que lo que edites en un worktree no afecte a los demás), veamos ahora qué pasa cuando toca juntar el trabajo de vuelta.

Estando en worktrees-playground\my-app:

cd ..\my-app.worktrees\feature-1\ # estoy en feature/1
echo foo > .\foo.txt # modificación
echo Baz > baz.txt # nuevo fichero
git add .
git commit -m "foo & Baz"
cd ..\..\my-app\ # y ahora estoy en main
git merge feature/1 # traer los cambios de feature/1 a main. Es un merge normal y corriente
git worktree remove ..\my-app.worktrees\feature-1\
git branch -d feature/1

Fíjate en que no hay ni un solo git switch. Cambiar de rama es literalmente cambiar de directorio. El resto, el merge incluido, es exactamente igual que siempre.

Dejando a un lado la línea de comandos, cualquier herramienta moderna entenderá los worktrees. Vamos a verlo con un worktree con cambios no confirmados:

De nuevo estando en worktrees-playground\my-app:

git reset --hard HEAD~1 # deshacer el merge anterior y volver al commit inicial
git worktree add ..\my-app.worktrees\feature-1 -b feature/1 main
cd ..\my-app.worktrees\feature-1\
echo foo > .\foo.txt
echo Baz > baz.txt
cd ..\..\my-app\
code .

Ahora en Visual Studio Code y teniendo activado el setting git.detectWorktrees verás algo así:

VSCode

Para ver los worktrees en la pestaña Source Control, tienes que abrir el repositorio principal. Es decir, abrir worktrees-playground\my-app. Si abres worktrees-playground\my-app.worktrees\feature-1 solo estarás abriendo ese linked worktree.

Por otro lado, cabe mencionar que Visual Studio Code tiene comandos específicos para trabajar con worktrees:

VSCode commands

Especial atención merecen estos 2:

  • Git: Compare with Workspace
  • Git: Migrate Worktree Changes...

Compare with Workspace permite seleccionar un fichero de un workspace y compararlo con el workspace actual. Por ejemplo, comparar foo.txt de feature-1 con foo.txt de main.

Migrate Worktree Changes… te permite seleccionar desde qué worktree traer los cambios para fusionar en el worktree actual (además de deshacer los cambios en el worktree seleccionado). Básicamente, “traer los cambios desde feature-1 a main y dejar feature-1 limpio”.

Migrate Worktree Changes

De todas formas, fíjate en que esto no es un merge (se trae solo los cambios que tiene en el working directory, si has hecho algún commit en el linked worktree no se los va a traer, eso sería ya un merge normal y corriente y no aplica para este comando), es algo que te da Visual Studio Code como un atajo. Básicamente es lo mismo que hacer lo siguiente (el stash se comparte entre worktrees):

git stash -u
cd ..\..\local\
git stash pop

Incluso Visual Studio también ha incluido recientemente soporte para worktrees. Está claro que, hoy por hoy, ninguna herramienta puede hacer caso omiso de la tendencia dominante.

Volviendo al origen del post, decíamos que los worktrees están viviendo una segunda juventud por el empuje de la IA. Y es que son una excelente opción para ejecutar varios agentes trabajando cada uno en su rama, sin pisarse.

Además de movernos a una ruta donde haya un worktree y arrancar desde allí CC (es lo más obvio y funciona), también podemos arrancar CC en un nuevo worktree con claude --worktree o claude --worktree <name>.

  • Si no especificamos nombre, el worktree se creará en .claude\worktrees\<random_name> y la rama se llamará worktree-<random_name>
  • Si especificamos nombre, el worktree se creará en .claude\worktrees\<name> y la rama se llamará worktree-<name>

También podemos abrir CC y pedirle directamente que haga un worktree. Algo tan simple como “Crea un worktree para trabajar en el bug de la carga de imágenes”… y ya.

O configurar que un subagente se ejecute en un worktree usando isolation: worktree en su frontmatter.

Lo que sí aparece al hilo de usar worktrees con CC, ess el concepto de que un worktree puede estar “bloqueado”. Por ejemplo:

git worktree add ..\my-app.worktrees\feature-1 -b feature/1 main
git worktree lock ..\my-app.worktrees\feature-1\ --reason "está en un disco externo"
git worktree list # el worktree aparecerá locked y nos mostrará reason
git worktree unlock ..\local.worktrees\feature-1\ # desbloquear
# ahora ya podemos borrarlo. Aunque con doble --force siempre podemos saltarnos el bloqueo
git worktree remove -f -f ..\local.worktrees\feature-1\

Otro punto a tener en cuenta es que, cuando creamos un worktree, no se copia nada de lo ignorado por .gitignore. CC permite añadir un fichero .worktreeinclude para arrastrar ficheros ignorados (por ejemplo un fichero .env).

Con node_modules lo recomendable es ejecutar npm install en el worktree. Si trabajas con pnpm la solución pasa por usar global virtual store. Con esto, todos los paquetes se guardan en un único directorio compartido y el node_modules de cada worktree contiene solo symlinks apuntando ahí.

El hook WorktreeCreate te permite tomar el control de la creación del worktree y automatizar npm install.

En cualquier caso, si no usas pnpm y tu npm install tarda minutos, tendrás que lidiar con ello.

Para aplicaciones de .NET es más sencillo porque los paquetes se guardan en el directorio global %USERPROFILE%\.nuget\packages y los user secrets funcionan con el atributo UserSecretsId del proyecto y también se guardan fuera del worktree. Si acaso (y si usas Husky.Net) tendrás que hacer dotnet husky install en el nuevo worktree.

Un saludo!

git