Forkeando repositorios con jj

“Fork” es una de las palabras más usadas en el trabajo diario con GitHub, GitLab, Gitea, Forgejo o Bitbucket, y no existe ni en git ni en jj. git help no tiene ningún comando fork, y jj help tampoco. Todo lo que hace que forkear parezca una extensión natural del control de versiones es una funcionalidad de la plataforma de hosting, añadida sobre una herramienta que solo entiende de repositorios, commits y refs - y esa frontera jj ni siquiera la toca. Los ejemplos de abajo usan jj en todo momento, en modo colocated.

Un fork es una funcionalidad de la plataforma, y jj hereda ese vacío sin cambios. Cuando una plataforma forkea un repositorio, crea un segundo repositorio, propiedad de otra cuenta o grupo, cuyo historial empieza siendo idéntico al original. Desde el punto de vista de git - y jj nunca lo contradice en este punto - son simplemente dos repositorios independientes que comparten un commit ancestro común. Ni .git ni nada que jj añada encima registra “esto es un fork de aquello”. Esa asociación vive por completo en la base de datos de la plataforma, accesible desde su interfaz web o su API.

Dos repositorios que comparten historial en el punto del fork y luego divergen de forma independiente

jj renombra el vocabulario local, no la mecánica de los remotos. Branch pasó a ser bookmark, HEAD pasó a ser @, pero jj git clone, jj git remote, jj git fetch y jj git push siguen hablando con los mismos remotos git, sobre los mismos protocolos, que sus equivalentes en git. Clonar el propio fork y añadir el original como segundo remoto se lee casi como git con un prefijo delante:

jj git clone --colocate https://example.com/tu-usuario/project.git
cd project
jj git remote add upstream https://example.com/original/project.git
jj git fetch --remote upstream

origin y upstream siguen siendo solo nombres, sin nada que los imponga salvo la costumbre.

Dos repositorios alojados, upstream y origin, ambos conectados a un mismo clon local

Un bookmark que no te sigue es justo lo que hace que sincronizar aquí sea indoloro. Una rama de git con main como checkout se mueve en cada commit - por eso mantener el main de un fork impoluto exige disciplina: un commit descuidado y ya se ha desviado. Un bookmark de jj nunca se mueve solo, mientras nadie describa un commit encima de él. Y mientras eso se cumpla, sincronizar no es un merge ni un rebase, solo mover un puntero hasta donde el fetch ya aterrizó:

jj git fetch --remote upstream
jj bookmark set main -r main@upstream
jj git push --remote origin --bookmark main

Si git.auto-local-bookmark está desactivado, ese primer fetch deja el estado de upstream accesible solo como main@upstream hasta que jj bookmark track main@upstream lo promueve a algo que jj bookmark set pueda referenciar por su nombre simple.

Contribuir sigue significando empujar un bookmark al fork, nunca a upstream. Ahí casi nunca se tiene acceso de escritura, con o sin jj. Un bookmark se crea, se empuja a origin, y en el instante en que cruza al remoto deja de ser un bookmark - jj git push lo escribe como una rama git corriente, porque ese es el único vocabulario que el remoto, y la plataforma que lo lee, entienden.

jj new main -m "fix timeout"
jj bookmark create fix-timeout
jj git push --remote origin --bookmark fix-timeout

El pull o merge request se abre después desde la interfaz o la API de la plataforma, comparando esa rama con la rama destino de upstream - una comparación que la plataforma hace enteramente en términos de git, sin saber en ningún momento que hubo un bookmark de por medio.

Un commit hecho localmente, empujado al fork, y luego propuesto y fusionado en upstream

El modo colocated hace que el mecanismo previo a las plataformas nunca haya desaparecido. Un repositorio bare servido por SSH, sin GitHub, GitLab o una capa similar encima, no tiene ningún concepto de fork ni ningún pull request que abrir. jj git push y jj git fetch funcionan igual de bien contra él - siempre funcionaron contra cualquier remoto git, hubiera plataforma o no -, pero el paso de “proponer un cambio” vuelve a lo que git ya soportaba antes de que existiera cualquiera de estas plataformas. Como el modo colocated mantiene un .git real justo al lado, git format-patch y git am siguen tan disponibles como siempre. jj nunca intentó reemplazar esa capa, así que ahí no había nada que perder.

Renombrar o eliminar el repositorio upstream es tan invisible para jj como lo es para git. Una entrada bajo jj git remote sigue siendo solo una URL; jj no tiene más vocabulario que git para “la plataforma movió esto debajo de ti”. Ese punto ciego nunca fue contabilidad local - está al otro lado de la misma frontera donde vive el propio forkeo, la frontera que jj decidió deliberadamente no cruzar.