Cómo está construido jj realmente por debajo

Parte 4 de 9.

El modelo mental del artículo anterior (la copia de trabajo como un commit, el identificador de cambio separado del identificador de commit) es la historia de cara al usuario. Tras leer los documentos de diseño y la disposición del código fuente de jj durante un rato más de lo estrictamente necesario, la arquitectura subyacente explica por qué ese modelo se sostiene, y por qué el modo colocado no se siente como ejecutar dos sistemas de control de versiones pegados con cinta adhesiva.

El almacén de commits es un backend intercambiable, y Git es una implementación de él. jj define su propia noción interna de un commit, y un trait de backend decide cómo se convierte eso en bytes en disco. El backend de Git es el predeterminado y el que usa todo este curso; da la casualidad de que almacena los commits de jj como objetos commit de Git reales. Ese es el mecanismo real detrás de “todo sigue siendo un repo Git normal por debajo”: no es una capa de traducción atornillada después, es la ruta de almacenamiento primaria.

El modo colocado no duplica el almacenamiento de objetos. .jj guarda estado específico de jj (el registro de operaciones, la vista actual, los datos de seguimiento de la copia de trabajo), pero los commits, árboles y blobs en sí viven en .git/objects, los mismos objetos que lee el propio Git. Por eso borrar .jj deja detrás un repositorio Git ordinario totalmente intacto: nunca hubo una segunda copia del contenido real que reconciliar.

Los identificadores de cambio viajan dentro del commit de Git, invisibles para Git. Git no tiene un concepto nativo de una identidad estable que sobreviva a una reescritura: un commit nuevo es simplemente un objeto nuevo con un hash nuevo, punto. jj obtiene su identificador de cambio metiendo un identificador opaco extra dentro del objeto commit de Git subyacente, en un lugar que las herramientas Git ordinarias ignoran. Git ve un commit normal; jj además ve la identidad que lo ata al historial del cambio a lo largo de ediciones y rebases.

El registro de operaciones es un DAG, no un historial lineal, y eso es deliberado. Cada comando de jj produce una operación nueva, y cada operación registra una instantánea completa del estado del repo (cada bookmark, cada cabeza) más un enlace de vuelta a la operación o operaciones a las que siguió. Dos terminales ejecutando jj contra el mismo repo al mismo tiempo producen dos operaciones con el mismo padre: una bifurcación, estructuralmente idéntica a dos commits que comparten un padre. El siguiente comando de jj que mire el repo fusiona esas operaciones bifurcadas automáticamente, la misma maquinaria de fusión a tres bandas que se usa para fusionar contenido de archivos, solo que aplicada a “qué aspecto tiene el repo”. Esa es una respuesta genuinamente distinta al acceso concurrente que el bloqueo de archivos de Git alrededor de actualizaciones individuales de referencias, y es lo que hace que ejecutar jj desde dos shells contra un repo no tenga nada de particular en lugar de ser una condición de carrera.

Nada se recolecta como basura solo por no estar referenciado por un bookmark. Como la vitalidad se rastrea a través del registro de operaciones en lugar de a través de referencias con nombre, como funciona el propio GC de Git, un commit que no está adjunto a ningún bookmark (un experimento del “trabajo nuevo sobre un elemento pasado” de la lección 5) se mantiene totalmente intacto y visible para jj log hasta que se abandona de verdad o el historial de operaciones que lo referencia envejece y desaparece. Las cabezas anónimas son algo de primera clase y permanente que tener por ahí, no una carrera de GC esperando a suceder.

Nada de esto cambia nada de los comandos de las demás lecciones. Es la parte que me hizo confiar en la herramienta en lugar de solo disfrutar de su interfaz: las propiedades de seguridad (corrección ante el acceso concurrente, un undo que lo cubre todo, un modo colocado genuinamente sin pérdidas) vienen de decisiones en esta capa, no de una disciplina cuidadosa apilada encima de un núcleo frágil.