Saltar al contenido

Jujutsu no tiene un paso para «crear una rama», y así por fin entendí las ramas de Git

Publicado el

Autora de «Juju-chu!» y de la serie «Riakuto!»

¿Jujutsu no tiene ramas?

Cuando empecé a usar Jujutsu después de años de trabajar con Git y acostumbrarme a sus flujos de trabajo, lo primero que me desconcertó fue que no había ninguna rama a la que perteneciera mi trabajo actual. Si empiezas con Git, te enseñan la rutina: antes de ponerte a trabajar en serio, crea una rama. Incluso en un proyecto personal en el que te saltas ese paso, al menos estás en la rama main.

Sin embargo, la documentación oficial y el popular tutorial se lanzan directamente al trabajo. Nadie crea una rama ni se cambia a otra. De hecho, por más que busqué, no encontré en Jujutsu nada llamado «rama» (branch) que se pudiera manipular. En su lugar hay algo llamado bookmark, que se mapea como una rama al trabajar con Git. Y aun así, un bookmark no es lo mismo que una rama. No entendía nada.

Pero después de hacer push de mi trabajo en Jujutsu y abrir PRs con él unas cuantas veces siguiendo las guías, se me ocurrió algo: quizá Git y Jujutsu simplemente se refieren a cosas distintas con la palabra «rama». Con esa idea en mente, volví a leer el capítulo sobre ramas de Pro Git1 y la página sobre bookmarks de la documentación de Jujutsu, y por fin todo encajó. Una rama de Git no es una «rama» en sentido literal, y hay buenas razones para que Jujutsu llame «bookmark» a su equivalente.

Qué significa realmente «una rama de Git es un puntero»

Entonces, ¿qué es exactamente una rama de Git? La sección 3.1 de Pro Git describe las ramificaciones así:

Cuando hablamos de ramificaciones, significa que tú has tomado la rama principal de desarrollo (master) y a partir de ahí has continuado trabajando sin seguir la rama principal de desarrollo.

Si sigues leyendo, encuentras esto:

Una rama Git es simplemente un apuntador móvil apuntando a una de esas confirmaciones.
[…]
¿Qué sucede cuando creas una nueva rama? Bueno…, simplemente se crea un nuevo apuntador para que lo puedas mover libremente.

«Una rama es un puntero» (o apuntador, en inglés pointer). Si llevas un tiempo usando Git, seguro que has oído esa frase. Me da algo de vergüenza admitirlo, pero nunca me dijo gran cosa, y durante años usé las ramas por pura intuición (al tanteo). Ahora entiendo por qué. La explicación se apoya en varias premisas que nadie menciona, y a quienes tuvieron Git como primer VCS no les dice mucho. La documentación oficial de Git y los libros clásicos los escribieron veteranos que conocían bien otros sistemas de control de versiones, y es fácil que pasen por alto que quienes empezaron con Git no comparten esas premisas.

¿Qué hace falta saber, entonces, para que «una rama es un puntero» tenga sentido? Lo primero es que, en Git, una rama no es la bifurcación del historial en sí. Para distinguir ambas cosas, Pro Git usa expresiones como «el historial diverge» (divergent history) para referirse a la bifurcación misma2.

La mayoría de los VCS distribuidos representan el historial como un DAG3 y no como un árbol, así que las bifurcaciones y las fusiones se expresan por completo dentro del grafo. En lo que se diferencian los VCS es en el mecanismo que le dan al usuario para ver y manipular esas bifurcaciones, y la mayoría llama «rama» a ese mecanismo. En Mercurial, que apareció más o menos al mismo tiempo que Git, una rama es un metadato que queda incrustado de forma permanente en cada commit de una línea del historial. Git, en cambio, usa un puntero por cada línea del historial, y cada uno apunta a un solo commit.

¿Todavía no queda claro? Pasemos a la segunda premisa. Lo que distingue a Git de otros VCS es que, estés donde estés, siempre te lleva automáticamente a la punta del historial. En los VCS anteriores, crear ramas y cambiar de una a otra eran operaciones pesadas que ralentizaban seriamente proyectos grandes como el kernel de Linux. Hacerlas rapidísimas fue uno de los objetivos clave al diseñar Git. La solución fue convertir la rama en un puntero móvil a un solo commit, guardado en un archivo de texto de apenas unas decenas de bytes.

Para que eso funcione, Git espera que edites el código en la línea de trabajo a la que apunta alguna rama, y el puntero de la rama avanza automáticamente con cada nuevo commit. Cambiar de rama significa hacer que HEAD, el puntero que indica dónde estás, coincida con esa rama. Por eso en Git, vayas adonde vayas, siempre terminas en la punta más reciente del historial.

Como Git era el único VCS distribuido que conocía, daba por hecho que así funcionaban todos. Descubrir que ese comportamiento es propio de Git, y entender el mecanismo que hay detrás, fue lo que por fin me permitió entender qué significa que una rama de Git sea un puntero.

¿Qué pasa con una bifurcación del historial sin rama?

Entonces, ¿puede haber en Git una bifurcación del historial que no sea una rama? Crear una es muy fácil: haz checkout de un commit anterior usando su hash.

$ git checkout 9effb1cc737824d9c14e1a8ecbb725e44ccae104
Note: switching to '9effb1cc737824d9c14e1a8ecbb725e44ccae104'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
⋮
HEAD is now at 9effb1c create b.txt
$ echo '444' > d.txt
$ git add .
$ git commit -m 'create d.txt'
[detached HEAD 46b43c3] create d.txt
1 file changed, 1 insertion(+)
create mode 100644 d.txt

Git se toma la molestia de advertirte que ahora estás en estado de detached HEAD (HEAD desacoplado). Normalmente HEAD apunta a alguna rama. Cuando, en cambio, apunta directamente al hash de un commit concreto, se dice que HEAD está desacoplado. Acumular commits en este estado se considera arriesgado y conviene evitarlo, porque ese historial termina así:

  1. No lo puedes encontrar después
  2. Se trata como basura y termina borrándose
  3. No lo puedes compartir con nadie

En Git, los commits solo se vuelven rastreables para las personas cuando pertenecen a una rama o a un tag. Los commits que haces con HEAD desacoplado pierden su referencia en cuanto cambias de rama, y ni siquiera aparecen en git log --all. El reflog es la única forma de recuperarlos. Además, el sistema los considera historial innecesario y quedan expuestos a la recolección de basura, así que, una vez vencido el plazo, desaparecen para siempre. Si son commits que tú mismo puedes perder de vista con facilidad y que pueden desaparecer en cualquier momento, difícilmente se pueden compartir con otras personas.

Por qué Jujutsu tiene bookmarks en lugar de ramas

Como dije al principio, Jujutsu no tiene nada llamado «rama» que se pueda manipular. Aun así, la palabra «branch» aparece por todas partes en su documentación, con su sentido literal de bifurcación del historial. Jujutsu no tiene el concepto de «rama actual» ni ningún mecanismo específico para identificar las bifurcaciones del historial, así que «branch» se usa en su sentido original y literal.

Entonces, ¿por qué Jujutsu no tiene ramas como las de Git o Mercurial? En Git, hacer amend o rebase de un commit le da un hash completamente nuevo. Los hashes que cambian constantemente son difíciles de seguir, tanto para el sistema como para las personas, así que un commit necesita pertenecer a una rama: una etiqueta con nombre, estable, que indica en qué línea de trabajo está. En Jujutsu, en cambio, la unidad básica del historial, el change, tiene un change ID inmutable, distinto de su commit ID, que cambia cada vez que se revisa el change. Sin importar cuántas veces reescribas su contenido o le hagas rebase, puedes seguir un change con el mismo ID, así que no hace mucha falta etiquetar cada línea de trabajo.

Además, las bifurcaciones del historial siempre se muestran en el log como un grafo en arte ASCII. Entre el grafo que ves en el log y las descripciones de los changes que forman sus nodos, salta a la vista a qué línea de trabajo pertenece cada change. Y cualquier change que tenga descripción o que modifique archivos se queda en el historial hasta que lo descartes explícitamente.

Log de Jujutsu

Como las ramas no tienen nombre, en Jujutsu lo normal es trabajar en el equivalente al HEAD desacoplado de Git. Jujutsu lo llama rama anónima (anonymous branch). El HEAD desacoplado se considera peligroso, pero las ramas anónimas de Jujutsu no tienen los dos primeros problemas que ese estado tiene en Git: «1. No lo puedes encontrar después» y «2. Se trata como basura y termina borrándose». ¿Y qué pasa con «3. No lo puedes compartir con nadie»? Para eso Jujutsu ofrece los bookmarks.

Un bookmark es un puntero con nombre asociado a un change. Tanto los bookmarks como las ramas de Git son punteros con nombre, pero, a diferencia de una rama de Git, un bookmark es solo una marca para compartir con otras personas o para no olvidarte de algo, sin relación directa con las bifurcaciones del historial. Por eso puedes poner varios bookmarks en la misma línea de trabajo, y también por eso, cuando creas nuevos changes sobre ese change, un bookmark no avanza por sí solo como lo hace una rama de Git.

Jujutsu no ve la necesidad de ponerles nombre a las bifurcaciones del historial, ni de señalar una línea de trabajo divergente completa solo para compartir historial con otras personas. Por eso Jujutsu tiene bookmarks en lugar de ramas.

Ramas anónimas + bookmarks: hacia dónde ha llegado la evolución de los VCS

En Git, tanto crear bifurcaciones del historial como fusionarlas se hace, en principio, a nivel de rama. ¿Y cómo lo resuelve Jujutsu, que no tiene ramas?

En Jujutsu, el comando que crea un change nuevo, la unidad básica del historial, es jj new. ¿Cómo se crea una bifurcación del historial? Pasándole a jj new el ID de un change que ya tiene un hijo. ¿Cómo se fusiona un historial que diverge? Pasándole a jj new los IDs de los changes que quieres fusionar. En Jujutsu, extender el historial, bifurcarlo y fusionarlo se hace a nivel de change, y, sorprendentemente, todo es la misma operación básica: crear un change nuevo.

Sigues trabajando así hasta un punto en el que tiene sentido parar. Es hora de abrir un PR. Es justo en este punto donde necesitas ponerle nombre a esa línea de trabajo4. Pones un bookmark en un change, haces push, y toda la serie de changes, desde donde se separó de main hasta ese change, queda registrada como una rama en el repositorio Git remoto, con cada change convertido en un commit de Git.

¿Qué se gana cuando la unidad para compartir no está atada a las ramas? En primer lugar, flexibilidad: puedes empezar a trabajar sin decidir un nombre ni qué vas a compartir, y resolver esas cuestiones solo cuando llegue el momento de compartir. En segundo lugar, facilita armar una serie de PRs dependientes, algo que últimamente se ha vuelto mucho más valioso. Los stacked pull requests de GitHub, que permiten dividir un PR en unidades pequeñas con relaciones padre-hijo, por fin pasaron a disponibilidad general el 6 de octubre de 20265, y con los bookmarks de Jujutsu es muy fácil trabajar con ellos. Para armar stacked PRs con Git, creas la primera rama con gh stack init, creas una rama hija con gh stack add cada vez que terminas una parte del trabajo y, al final, haces push y abres los PRs con gh stack submit. Es un proceso bastante tedioso y dependiente de herramientas específicas. Con Jujutsu, pones bookmarks en orden sobre tu línea de trabajo, haces push y ejecutas algo como gh stack link <bookmark1> <bookmark2> <bookmark3>. Eso es todo. Es casi anticlimático de lo fácil que resulta.

Al no tener ramas, Jujutsu replanteó las operaciones que Git había construido en torno a ellas, de modo que puedes trabajar en unidades más pequeñas y flexibles. Después de convivir un tiempo con un Jujutsu sin ramas, por fin entendí qué son las ramas de Git, después de años de usarlas con una idea vaga. Y entonces los conceptos de Jujutsu, tan distintos de los de Git, empezaron a parecerme sorprendentemente lógicos. Esa sensación se vuelve aún más fuerte cuando tomas distancia y miras la historia de los VCS.

Ya mencioné que Mercurial tiene un tipo de rama distinto del de Git. Pero como las ramas de Mercurial son datos permanentes y globales incrustados en cada commit, no encajan bien con el desarrollo de funcionalidades o las correcciones de bugs que se terminan en pocos días. Por eso más tarde se agregaron los bookmarks para el trabajo liviano: marcas de tipo puntero que servían para pasar el trabajo al hacer push o para señalar dónde estabas trabajando. Inspirados en las ramas de Git, pero independientes de las ramas de Mercurial y pensados para compartir. De ahí vienen los bookmarks de Jujutsu.

Jujutsu va un paso más allá: considera que las ramas son innecesarias para las bifurcaciones del historial y directamente no las tiene. Curiosamente, Sapling, que Meta creó a partir de un fork de Mercurial y desarrolló por su cuenta, tampoco tiene ramas, pero sí bookmarks. Se podría decir que «ramas anónimas + bookmarks» es uno de los destinos a los que ha llegado la evolución del control de versiones, de Git a Jujutsu pasando por Mercurial.

Una última aclaración: este artículo se basa en las secciones «2-4-2. La diferencia entre branch y bookmark» y «2-4-3. Trabajar en branches anónimas» de mi libro, Juju-chu! —Comienza tu flujo de trabajo Jujutsu × IA con `jj new`.

Juju-chu! —Comienza tu flujo de trabajo Jujutsu × IA con `jj new`Una guía práctica para adoptar Jujutsu (jj) junto a Claude Code y Codex: snapshots automáticos, operaciones que siempre se pueden deshacer y una reescritura del historial que es segura.juju-chu.com

Los primeros capítulos del libro se centran en la diferencia entre los modelos mentales de Git y de Jujutsu. Además de las ramas frente a los bookmarks, explican con cuidado los otros puntos en los que suelen tropezar quienes empiezan. Si este artículo te dio ganas de probar Jujutsu, o de darle otra oportunidad después de que la primera vez no te convenciera, échale un vistazo.

Footnotes

  1. Libro de código abierto sobre Git escrito por Scott Chacon y Ben Straub, que también se usa como documentación oficial de Git. Se puede leer gratis en línea. https://git-scm.com/book/es/v2 ↩

  2. https://git-scm.com/book/es/v2/Ramificaciones-en-Git-%C2%BFQu%C3%A9-es-una-rama%3F#rdivergent_history ↩

  3. Siglas en inglés de Directed Acyclic Graph (grafo acíclico dirigido): una estructura de grafo en la que los nodos, unidos por aristas con dirección, nunca forman un ciclo. ↩

  4. Si prefieres ponerle nombre desde el principio, por supuesto también puedes hacerlo. Crea un bookmark con jj bookmark create al empezar a trabajar y, antes de hacer push, muévelo al change que corresponda con jj bookmark move o jj bookmark advance. ↩

  5. Stacked pull requests generally available - GitHub Changelog ↩