De Git a Jujutsu, paso a paso
Si conoces bien Git, jj te resultará familiar en algunos aspectos y del revés en otros. Este tutorial recorre el camino flujo a flujo, mostrando siempre lo que harías en Git junto a lo que haces en jj y, cuando ayuda, el movimiento equivalente en Wyrm.
No necesitas memorizar comandos nuevos de entrada. Empieza por el cambio de mentalidad y luego sigue las fases en orden.
El cambio de mentalidad fundamental
Sección titulada «El cambio de mentalidad fundamental»Un puñado de ideas explica casi todas las diferencias con las que te vas a encontrar:
- No hay staging area. Cada cambio en un archivo forma parte inmediatamente del commit actual.
- La copia de trabajo es un commit, marcado como
@en el log. Cada comando de jj toma automáticamente una instantánea de tus archivos dentro de él. - Los commits son baratos y desechables. No haces squash para «limpiar antes del push»; haces squash cuando quieres, y jj se encarga del efecto en cascada.
- Reescribir el historial es el flujo de trabajo normal, no una vía de escape peligrosa.
- Las ramas anónimas son lo normal. El temido «detached HEAD» de Git es el día a día en jj.
- Los bookmarks son etiquetas de envío, no espacios de trabajo. No trabajas «sobre un bookmark»: trabajas sobre commits y etiquetas uno cuando llega el momento de hacer push.
En Wyrm, la fila superior del grafo del log es @. A medida que editas archivos, esa fila se actualiza en directo: no hay un panel aparte de «cambios sin confirmar» que gestionar.
Paso 1 — Empezar a trabajar
Sección titulada «Paso 1 — Empezar a trabajar»En Git creas una rama, editas, preparas los cambios (stage) y haces commit. En jj simplemente empiezas a editar.
En Git…
# 1. You're on developgit checkout -b feature/new-ui # branch BEFORE you work# 2. Edit files (now in a dirty, unsaved state)# 3. Stage and commitgit add src/ui/*.ts && git commit -m "feat: new ui"En jj…
# 1. You're on develop (same)# 2. Just start editing. That's it.# No branch. No staging. Every save is captured in @.# 3. Name the work when you're ready:jj describe -m "feat: new ui"# 4. (Optional, only when pushing) add a bookmark:jj bookmark create feature/new-uiTu trabajo está a salvo en cuanto guardas un archivo. No necesitas ninguna rama hasta que hagas push, y no hay «staging»: todo vive en @.
Paso 2 — Retomar trabajo antiguo
Sección titulada «Paso 2 — Retomar trabajo antiguo»Tienes un commit antiguo con buenos cambios, pero develop ha avanzado 20 commits. ¿Cómo te pones al día?
El truco: el orden da igual. Haz primero el rebase o edita primero: a jj le da lo mismo, y los conflictos no te van a bloquear.
Opción A — primero rebase, luego editar (lo más habitual):
# Move your old commit onto the latest develop:jj rebase -r old_change_id -d develop
# Then continue working on it:jj edit old_change_id# Now @ IS that commit; edits amend it directly.# The change ID stays the same even after the rebase.Opción B — primero editar, rebase después:
jj edit old_change_id # jump to it right now# ...work...jj rebase -r @ -d develop # rebase when you're readyEn Git esto es «checkout → rebase → resolver conflictos → continue», y un conflicto te deja congelado a mitad del rebase. En jj puedes hacer el rebase, ver un conflicto, ponerte con otra cosa y volver más tarde para resolverlo: el conflicto es simplemente un estado en el que se encuentra el commit.
Paso 3 — Commits inmutables
Sección titulada «Paso 3 — Commits inmutables»¿Has hecho doble clic en un commit antiguo y te ha salido «Revision is immutable»? En Git puedes hacer checkout de cualquier sitio, así que parece que jj se interpone en tu camino. No es así.
jj protege los commits que ya son públicos, es decir, compartidos con el mundo:
| Inmutable (◆) | Mutable (●) |
|---|---|
Commits en main@origin |
Tu copia de trabajo @ |
Commits en cualquier *@origin |
Commits solo locales |
| Commits con tag | Todo lo que aún no has subido con push |
Por qué es bueno: en Git puedes hacer git push --force sobre main o enmendar un commit compartido y destrozar un historial del que dependen otros, y nada te lo impide. jj te dice «esto ya es público; no puedes reescribir aquello en lo que confía el equipo». Eso elimina toda una categoría de momentos de «ay, no».
Qué hacer en su lugar: no edites el commit inmutable; construye uno nuevo encima:
jj new immutable_change_idTu nuevo @ contiene todos los archivos de ese commit; editas con total libertad y el original queda intacto.
| Commit mutable (●) | Commit inmutable (◆) |
|---|---|
jj edit change_id |
jj new change_id |
@ se mueve a ese commit |
Crea un nuevo commit hijo |
| Las ediciones lo enmiendan en el sitio | @ es el nuevo hijo |
Referencia rápida: new vs edit vs describe vs commit
Sección titulada «Referencia rápida: new vs edit vs describe vs commit»| Comando | Qué hace | Equivalente más cercano en Git | Cuándo usarlo |
|---|---|---|---|
jj new |
Nuevo commit hijo vacío; mueve @ allí |
git checkout -b |
«Terminé con esto, a lo siguiente». |
jj edit |
Mueve @ a un commit mutable existente |
git rebase -i → edit |
Volver atrás para arreglar un commit anterior. |
jj describe |
Pone o cambia el mensaje de @ |
git commit --amend -m |
Ponerle nombre a lo que estás haciendo. |
jj commit |
describe + new de una vez |
git commit -m |
Terminar este trabajo y empezar el siguiente. |
Paso 4 — Commits parciales y limpieza
Sección titulada «Paso 4 — Commits parciales y limpieza»Has editado 10 archivos. Solo 7 pertenecen a este commit; 3 son basura de pruebas. Algunos archivos incluso mezclan cambios reales con líneas desechables.
Git es opt-in: preparas lo que quieres y descartas el resto. jj es opt-out: todo está ya en @, así que quitas lo que no quieres. Mismo destino, dirección opuesta.
Método 1 — jj restore (descartar archivos enteros):
# Revert these files to the parent's version:jj restore test-setup.ts dev-nav-hack.svelte .env.test# @ now has only the 7 real files. Name it:jj describe -m "fix: navigation and auth"jj restore es el equivalente de «Descartar cambios»: devuelve un archivo a su estado en el commit padre.
Método 2 — jj split (quedarte con una parte y separar el resto):
jj split # pick what goes into the FIRST commit# Result:# ○ @ — leftover (3 test files)# ● fix: navigation and auth (7 real files)# ◆ developjj bookmark create nav-fix -r @-jj git push --bookmark nav-fixjj abandon @ # throw away the test-junk commitArchivos mezclados (a nivel de hunk): cuando un archivo tiene 10 líneas reales y 5 apaños de prueba:
jj split -iEsto abre un diff interactivo con cada hunk modificado; eliges qué hunks van al primer commit y cuáles se quedan en el segundo. Es el git add -p de jj.
¿Basura de pruebas repartida en varios commits? No necesitas un split por commit:
# Option A — squash everything, then split once:jj squash --from "commit1::commit5" --into commit1jj split -i -r that_commitjj abandon junk_commit# Option B — jj absorb:# 1. In @, simply UNDO the test changes (delete the files,# remove the test lines). jj snapshots those "undo" edits.# 2. Route them home automatically:jj absorbjj absorb examina cada hunk de deshacer en @, encuentra el commit ancestro que añadió esas líneas e integra la corrección en ese commit: sin rebase interactivo, con un solo comando. Más sobre esto en el Paso 8.
Paso 5 — Squash, rebase y conflictos
Sección titulada «Paso 5 — Squash, rebase y conflictos»En Git…
git rebase -i HEAD~5# Editor opens; change "pick" → "squash" on 4 lines,# write the combined message, save.En jj…
# One at a time, into the parent:jj squash -r commit5jj squash -r commit4
# …or a whole range in one command:jj squash --from "commit1::commit5" --into commit1Los descendientes se rebasan automáticamente después de cada squash. Sin editor, sin --continue, sin ceremonias.
En Git…
git checkout feature/new-uigit fetch origingit rebase origin/develop# On conflict: STOP, fix files, git add ., git rebase --continue# …repeated for EACH conflicting commit.En jj…
jj git fetchjj rebase -r your_change -d develop# On conflict: nothing stops. The commit is just marked CONFLICT.jj edit your_change # fix the markers when you're ready — that's it.Qué commits se mueven depende del flag:
| Flag | Qué mueve | Análogo en Git |
|---|---|---|
-r (revision) |
Solo este commit; los hijos se quedan donde están | (sin equivalente directo) |
-s (source) |
Este commit y todos sus descendientes | git rebase --onto |
-b (branch) |
Todo lo que hay desde que divergió del destino | git rebase develop |
Conflictos — la mayor diferencia
Sección titulada «Conflictos — la mayor diferencia»Resolver uno es simplemente editar un archivo:
jj edit conflicted_change# Open the file. jj's markers show the base plus what each# side intended, not two opaque full versions:## <<<<<<< Conflict 1 of 1# %%%%%%% diff from base to side #1# context# -old line# +your change# +++++++ side #2# their change# >>>>>>> Conflict 1 of 1 ends## Fix it, save. jj snapshots. Resolved. No git add. No --continue.Como los marcadores describen la intención (la base + cada diff) en lugar de dos blobs finales, los conflictos suelen ser más fáciles de razonar. Consulta Resolución de conflictos para ver el flujo en Wyrm.
Todo junto: pasos 1 a 5
Sección titulada «Todo junto: pasos 1 a 5»Una funcionalidad completa, de principio a fin:
- Empieza a trabajar —
jj describe -m "feat: new navigation UI" - Captura hitos sobre la marcha —
jj commit -m "wip: nav routing done" - Límpialo en un solo commit —
jj squash --from "first::last" --into first - Quita la basura de pruebas —
jj restore test-setup.ts(ojj split -i) - Haz rebase sobre el develop más reciente —
jj git fetch && jj rebase -r feature -d develop - Etiqueta y haz push —
jj bookmark create feature/x && jj git push --bookmark feature/x - Abre la PR y consigue la revisión
- Atiende los comentarios —
jj edit feature_change→ haz los cambios →jj git push --bookmark feature/x
Sin commits de fixup, sin un segundo squash. Edita el commit y haz push: jj hace el force-push de forma segura por su cuenta.
Paso 6 — Bookmarks
Sección titulada «Paso 6 — Bookmarks»Un bookmark de jj no es una rama de Git. Una rama de Git sigue a HEAD: haces commit y se mueve contigo; siempre estás «en» una rama. Un bookmark de jj es una nota adhesiva pegada a un commit concreto. No se mueve cuando creas commits nuevos y nunca estás «en» él. Siempre estás en @.
Git: escribe la dirección en la caja antes de empaquetarla. jj: empaqueta la caja y pégale la etiqueta de envío cuando vayas a mandarla.
Así que primero trabajas y después etiquetas. Tres situaciones habituales cuando ya existe un bookmark antiguo:
A — el trabajo antiguo está obsoleto (tus nuevos commits lo sustituyen):
jj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesign # safe force-pushB — conservar el trabajo antiguo y construir encima:
jj rebase -s commit1 -d feature/nav-redesign # your commits on topjj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesignC — combinar el trabajo antiguo y el nuevo (archivos distintos, quieres ambos):
jj squash --from old_work_change_id --into commit1jj bookmark set feature/nav-redesign -r commit3jj git push --bookmark feature/nav-redesign| Comportamiento | Rama de Git | Bookmark de jj |
|---|---|---|
| ¿Se mueve al hacer commit? | Sí — sigue a HEAD automáticamente | No — se queda quieto; lo mueves a mano |
| ¿Hace falta para trabajar? | Sí — siempre estás «en» una | No — puedes trabajar sin ninguno |
| ¿Hace falta para hacer push? | Sí | Sí — un push necesita un nombre |
| ¿Hay uno «actual»? | Sí — HEAD apunta a él | No existe el concepto; @ es siempre el actual |
| ¿Sigue a un rebase? | A veces | Sí — sigue el change ID a través de las reescrituras |
| ¿Miedo al detached HEAD? | Sí | Ninguno — eso es lo normal |
| Force push | Manual, da miedo | Automático, seguro (with-lease) |
Como los bookmarks siguen el change ID (no el SHA del commit), sobreviven a las reescrituras: después de un rebase basta con hacer push, sin necesidad de un force-push manual para actualizar el remoto.
Paso 7 — Push, fetch y el ciclo de las PR
Sección titulada «Paso 7 — Push, fetch y el ciclo de las PR»Push — un verbo, no cuatro
Sección titulada «Push — un verbo, no cuatro»Git tiene varias formas de push (-u la primera vez, push a secas, --force-with-lease, --delete). jj las canaliza todas por un único comando:
jj git push --bookmark my-thing # first pushjj git push --bookmark my-thing # every push afterjj git push --all # all local bookmarksjj git push --delete old-bookmark # delete a remote bookmarkNo existe el flag --force. jj se comporta siempre como --force-with-lease: si alguien ha hecho push a ese bookmark desde tu último fetch, se niega y te pide que hagas fetch primero. Es literalmente imposible machacar por accidente el trabajo de un compañero.
Fetch — nunca una sorpresa
Sección titulada «Fetch — nunca una sorpresa»jj git fetch # downloads remote state; does NOT merge, # rebase, or touch your working copy.# Then YOU decide:jj rebase -d main@origin # rebase your work onto the update — or don't.No existe jj pull. El fetch y el rebase siempre van por separado, así que nunca te encontrarás con un commit de merge inesperado.
El ciclo de revisión
Sección titulada «El ciclo de revisión»Atiende los comentarios editando el commit en el sitio y volviendo a hacer push, sin commits de fixup:
jj edit feature_change# ...make the requested changes...jj git push --bookmark feature/x # auto force-push, safelyPaso 8 — Movimientos avanzados
Sección titulada «Paso 8 — Movimientos avanzados»Algunas capacidades de jj que no tienen un equivalente limpio en Git.
jj absorb — envía cada corrección a su commit automáticamente
Sección titulada «jj absorb — envía cada corrección a su commit automáticamente»El revisor dice: «la línea 42 del commit 2 está mal, y la línea 88 del commit 4 tiene una errata». En Git eso supone un git commit --fixup por cada punto más un git rebase -i --autosquash.
# 1. In @, just fix BOTH lines wherever they live now.# jj snapshots; @ holds both fixes.# 2. Route them home:jj absorbPara cada línea modificada, jj encuentra el ancestro más reciente que la tocó, lleva la corrección allí y rebasa los descendientes. Ambos errores quedan corregidos en sus commits correspondientes y @ vuelve a estar vacío.
Sin stash — cada estado ya es un commit
Sección titulada «Sin stash — cada estado ya es un commit»Tu jefe dice: «déjalo todo y arregla esto en caliente». En Git eso es git stash / rama / arreglo / git stash pop y cruzar los dedos.
jj new develop -m "hotfix: prod is down"# Your previous work is still there as a sibling commit.# Fix, push, then return:jj edit feature_a_id # right back where you left offNo hay stash porque no hay nada que guardar en él: tu trabajo nunca estuvo sin confirmar.
PR apiladas — reapilado automático
Sección titulada «PR apiladas — reapilado automático»Entrega cuatro PR pequeñas y fáciles de revisar que se construyen una sobre otra, en lugar de una gigante:
● commit 4 ← bookmark feature/tests● commit 3 ← bookmark feature/ui● commit 2 ← bookmark feature/api● commit 1 ← bookmark feature/auth-refactor◆ developEn Git, arreglar un error en la PR base implica hacer rebase y force-push de cada PR que tiene encima. En jj, editas el commit base, dejas que todos los descendientes se rebasen solos y actualizas todas las PR a la vez:
jj edit feature/auth-refactor # fix the base# ...descendants auto-rebase...jj git push --all # all four PRs update togetherChuleta jj ↔ git
Sección titulada «Chuleta jj ↔ git»Ciclo de vida del trabajo
| Objetivo | jj | git |
|---|---|---|
| Estado | jj status |
git status |
| Log | jj log |
git log --graph |
| Diff del trabajo actual | jj diff |
git diff HEAD |
| Empezar un commit nuevo | jj new |
git checkout -b |
| Poner el mensaje del commit | jj describe -m "…" |
git commit --amend -m |
| Hacer commit y seguir | jj commit -m "…" |
git commit -m |
Editar commits
| Objetivo | jj | git |
|---|---|---|
| Editar un commit mutable antiguo | jj edit <rev> |
git rebase -i (edit) |
| Hijo de cualquier commit | jj new <rev> |
git checkout -b |
| Integrar en el padre | jj squash |
fixup + autosquash |
| Integrar de forma interactiva | jj squash -i |
git add -p + amend |
| Dividir un commit | jj split |
git rebase -i (split) |
| Enviar correcciones automáticamente | jj absorb |
(ninguno) |
| Descartar un commit | jj abandon |
git reset --hard HEAD~1 |
| Restaurar un archivo | jj restore <file> |
git checkout HEAD -- <file> |
Operaciones con el remoto
| Objetivo | jj | git |
|---|---|---|
| Fetch | jj git fetch |
git fetch |
| Push | jj git push --bookmark <name> |
git push |
| Primer push | jj git push --bookmark <name> |
git push -u |
| Force push (automático) | jj git push --bookmark <name> |
git push --force-with-lease |
| Push de todo | jj git push --all |
git push --all |
| Borrar en el remoto | jj git push --delete <name> |
git push --delete |
Bookmarks
| Objetivo | jj | git |
|---|---|---|
| Crear | jj bookmark create <name> |
git branch <name> |
| Mover | jj bookmark set <name> -r <rev> |
git branch -f <name> |
| Borrar | jj bookmark delete <name> |
git branch -d <name> |
| Listar | jj bookmark list |
git branch -a |
Seguridad y selectores
| Objetivo | jj | git |
|---|---|---|
| Deshacer cualquier cosa | jj undo |
git reflog + reset |
| Historial de operaciones | jj op log |
git reflog |
| «Stash» del trabajo | jj new @- y luego jj edit de vuelta |
git stash |
| Revset | Significado |
|---|---|
@ |
Copia de trabajo |
@- |
Padre de la copia de trabajo |
trunk() |
Rama principal en origin |
trunk()..@ |
Tu trabajo local que aún no está en trunk |
mine() |
Tus propios commits |
conflicts() |
Todos los commits en conflicto |
bookmarks() |
Todos los commits con bookmark |
description("fix") |
Commits con «fix» en el mensaje |
Siguientes pasos
Sección titulada «Siguientes pasos»- Fundamentos de jj — los change ID, la copia de trabajo y el log de operaciones en profundidad.
- Grafo del log — navega y filtra el historial en Wyrm.
- Resolución de conflictos — resuelve conflictos de forma visual con el resolvedor de tres vías integrado.
- Vista previa de push — revisa exactamente qué llegará al remoto antes de hacer push.