Módulo 9: Laboratorio — rebase, cherry-pick y bisect
Unidad: GIT-11
Nivel: avanzado, opcional tras el itinerario troncal.
Público: alumnado de DAW, DAM, ASIR y grados de informática.
Itinerario: GIT-11, después de ramas, remotos, conflictos y recuperación (reflog).
Prerrequisitos: manejar log, ramas, merge, conflictos, restore, revert y reflog.
Objetivo observable: reorganizar con seguridad el historial de una rama no publicada, trasplantar un commit concreto a otra rama y localizar el commit que introdujo una regresión mediante búsqueda binaria.
Fuente técnica: documentación oficial de git-rebase, git-cherry-pick, git-bisect y git-reflog.
Encaje curricular: contenido transversal avanzado; no depende de GitHub o GitLab.
Laboratorio A — Rebase interactivo en una rama privada
Objetivo
Dejar una rama de funcionalidad preparada para revisión: actualizada sobre main, con dos commits coherentes en lugar de tres provisionales.
Idea clave
git rebase main vuelve a aplicar los commits exclusivos de la rama actual sobre la punta de main. Al hacerlo, crea commits nuevos: cambian sus identificadores.
Úsalo únicamente en trabajo local o en una rama que no esté siendo usada por otras personas. No cambia ningún remoto por sí solo.
Preparación reproducible
git init -b main laboratorio-rebase
cd laboratorio-rebase
git config user.name "Alumno"
git config user.email "alumno@example.test"
printf "# Tienda\n" > README.md
git add README.md
git commit -m "chore: inicia proyecto"
git switch -c feature/formulario
printf "<form></form>\n" > formulario.html
git add formulario.html
git commit -m "feat: añade estructura de formulario"
printf "required\n" >> formulario.html
git add formulario.html
git commit -m "wip: añade validación"
printf "Formulario de contacto.\n" >> README.md
git add README.md
git commit -m "docs: documenta formulario"
git switch main
printf "# Guía de desarrollo\n" > CONTRIBUTING.md
git add CONTRIBUTING.md
git commit -m "docs: añade guía de desarrollo"
git switch feature/formulario
git log --oneline --graph --allEl commit de main toca un archivo distinto al de la rama a propósito: así el
rebase de la Parte 1 se aplica limpio y puedes centrarte en el mecanismo. Los
conflictos durante un rebase se tratan más abajo, en su propia sección.
Salida esperada, con hashes distintos:
* <hash> docs: documenta formulario
* <hash> wip: añade validación
* <hash> feat: añade estructura de formulario
| * <hash> docs: añade guía de desarrollo
|/
* <hash> chore: inicia proyectoParte 1: actualizar la base de la rama
git rebase mainSalida esperada aproximada:
Successfully rebased and updated refs/heads/feature/formulario.Ahora los tres commits de feature/formulario son nuevos commits sobre la punta actual de main. El directorio de trabajo y el área de preparación deben quedar limpios:
git statusOn branch feature/formulario
nothing to commit, working tree cleanParte 2: ordenar y fusionar commits
git rebase -i mainEn el editor, dejar las líneas así:
pick <hash> feat: añade estructura de formulario
squash <hash> wip: añade validación
pick <hash> docs: documenta formularioAl guardar, Git pedirá el mensaje del commit fusionado. Sustituirlo por:
feat: añade formulario con validaciónComprobación:
git log --oneline main..HEADSalida esperada:
<hash> docs: documenta formulario
<hash> feat: añade formulario con validaciónQué cambia Git
- Directorio de trabajo e índice: durante el proceso pueden contener el estado intermedio del commit que Git está reaplicando; al acabar correctamente quedan en el estado de la nueva punta.
- Historial local: se sustituyen los commits originales por otros nuevos.
- Remoto: no cambia. Si esta rama ya se hubiese publicado, habría que coordinarlo y, solo si procede, actualizarla con
git push --force-with-lease, nunca con unpush --forceindiscriminado.
Si aparece un conflicto
git status
# editar los archivos con marcadores <<<<<<<, ======= y >>>>>>>
git add <archivo-resuelto>
git rebase --continueAlternativas:
git rebase --abort # vuelve la rama al estado previo al rebase
git rebase --skip # omite el commit actual; usar solo si se entiende la pérdidaSi se termina un rebase no deseado, localizar la punta anterior:
git reflog
git switch feature/formulario
git reset --hard <hash-anterior>reset --hard sobrescribe el directorio de trabajo y el área de preparación: usarlo solo tras identificar el commit correcto y comprobar que no hay cambios sin guardar.
Errores frecuentes
- Hacer
rebasesobre una rama compartida: las demás personas conservarán referencias a los commits antiguos. - Creer que
rebase“mueve” commits: los reaplica y crea otros nuevos. - Resolver un conflicto y olvidar
git add:git rebase --continueno podrá avanzar. - Usar
--skippara salir del paso: puede eliminar un cambio funcional necesario.
Ejercicio y criterios
Crea una rama con cuatro commits: dos de funcionalidad, uno de corrección y uno de documentación. Tras actualizarla sobre main, deja tres commits: una funcionalidad, una corrección y documentación.
Se considera correcto si:
git log --oneline main..HEADmuestra exactamente tres commits con mensajes claros.- El contenido funciona y
git statusqueda limpio. - La explicación identifica que los hashes cambiaron.
- No se ha modificado ningún remoto.
Laboratorio B — Trasplantar un commit con cherry-pick
Objetivo
Llevar a main una corrección concreta que vive dentro de una rama de funcionalidad sin arrastrar el resto de esa rama.
Idea clave
git cherry-pick <hash> toma el cambio que introdujo un commit y lo aplica sobre la rama actual como un commit nuevo.
La palabra importante es nuevo: no mueve el commit ni lo comparte. El contenido del cambio es el mismo, pero el commit resultante tiene otro hash, otra fecha de aplicación y otro padre. Es la misma idea que en rebase —reaplicar, no mover—, aplicada a un commit suelto en lugar de a una rama entera.
Cuándo usarlo, y cuándo no
cherry-pick resuelve bien un caso concreto: necesitas un cambio ya, y la rama que lo contiene no está lista.
| Situación | Herramienta |
|---|---|
| Un fix urgente atrapado en una rama a medias | cherry-pick |
| Un commit hecho por error en la rama equivocada | cherry-pick al sitio correcto y borrarlo del origen |
| Llevar unas correcciones a una rama de mantenimiento | cherry-pick de un rango |
| Integrar una rama terminada | merge (módulo 5) |
Poner la rama al día sobre main |
rebase (laboratorio A) |
Lo que no es: una forma de integrar ramas. Trasplantar commits uno a uno para no aprender merge acaba en historiales duplicados y difíciles de leer.
Preparación reproducible
git init -b main laboratorio-cherry
cd laboratorio-cherry
git config user.name "Alumno"
git config user.email "alumno@example.test"
printf "PRECIO_BASE = 100\nIVA = 0.21\n" > tarifas.py
git add tarifas.py
git commit -m "chore: inicia proyecto"
printf "# Tarifas\n" > README.md
git add README.md
git commit -m "docs: crea README"
git switch -c feat/rediseno
printf "\nDESCUENTO = 0.10\n" >> tarifas.py
git add tarifas.py
git commit -m "feat: añade descuento"
printf "IVA = 0.21 # tipo general vigente\n" > iva.py
git add iva.py
git commit -m "fix: corrige el tipo de IVA aplicado"
printf "\nCOLOR_PRIMARIO = 'azul'\n" >> tarifas.py
git add tarifas.py
git commit -m "feat: aplica nueva paleta"
git log --oneline --graph --allSalida esperada, con hashes distintos:
* d988099 feat: aplica nueva paleta
* 268f417 fix: corrige el tipo de IVA aplicado
* c1f4c49 feat: añade descuento
* 8bc7e0b docs: crea README
* 7f6b0e4 chore: inicia proyectoLa situación es la habitual: el rediseño va para largo, pero la corrección del IVA hay que publicarla hoy.
Parte 1: trasplantar el commit
git switch main
lsREADME.md
tarifas.pymain no tiene iva.py. Trasplanta solo ese commit, usando su hash:
git cherry-pick 268f417
ls
git log --oneline -2[main 36151b8] fix: corrige el tipo de IVA aplicado
1 file changed, 1 insertion(+)
create mode 100644 iva.py
README.md
iva.py
tarifas.py
36151b8 fix: corrige el tipo de IVA aplicado
8bc7e0b docs: crea READMEmain tiene la corrección y nada más: ni el descuento ni la paleta.
El commit es nuevo: mismo cambio, distinto hash
Compara el original con el trasplantado:
git log --oneline -1 feat/rediseno~1 # 268f417, el original
git log --oneline -1 main # 36151b8, el nuevo
diff <(git show 268f417 --format="") <(git show 36151b8 --format="")El diff no devuelve nada: el cambio es idéntico. Pero son dos commits distintos, y git log los mostrará como tales. Interiorizar esto evita la confusión clásica de «¿por qué aparece dos veces la misma corrección?».
Parte 2: dejar rastro del origen con -x
Como el hash cambia, conviene registrar de dónde vino:
git reset --hard HEAD~1
git cherry-pick -x 268f417
git log -1 --format="%B"fix: corrige el tipo de IVA aplicado
(cherry picked from commit 268f4179c5302a91c3ceb3374a3f7ec848b476e4)-x añade esa línea al mensaje. En un proyecto con ramas de mantenimiento es la diferencia entre poder rastrear una corrección y tener que adivinarlo. Úsalo siempre al trasplantar entre ramas de larga vida.
Parte 3: cuando hay conflicto
Un trasplante puede chocar con lo que ya hay en el destino:
git switch feat/rediseno
printf "PRECIO_BASE = 100\nIVA = 0.21\nENVIO = 5\n" > tarifas.py
git add tarifas.py && git commit -m "feat: añade coste de envío"
git switch main
printf "PRECIO_BASE = 120\nIVA = 0.21\n" > tarifas.py
git add tarifas.py && git commit -m "feat: sube el precio base"
git cherry-pick <hash-del-coste-de-envío>Auto-merging tarifas.py
CONFLICT (content): Merge conflict in tarifas.py
error: could not apply d5367d0... feat: añade coste de envío
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".El estado y los marcadores son los del módulo 5, con una particularidad:
git status --short
cat tarifas.pyUU tarifas.py
PRECIO_BASE = 120
IVA = 0.21
<<<<<<< HEAD
=======
ENVIO = 5
>>>>>>> d5367d0 (feat: añade coste de envío)UU es el código corto de «ambas versiones modificadas». Fíjate en que el marcador inferior identifica el commit trasplantado, no una rama: es la pista de que estás en un cherry-pick y no en un merge. Resuelve, prepara y continúa:
printf "PRECIO_BASE = 120\nIVA = 0.21\nENVIO = 5\n" > tarifas.py
git add tarifas.py
git statusOn branch main
You are currently cherry-picking commit d5367d0.
(all conflicts fixed: run "git cherry-pick --continue")
(use "git cherry-pick --skip" to skip this patch)
(use "git cherry-pick --abort" to cancel the cherry-pick operation)git cherry-pick --continueComo en el rebase, hay tres salidas y conviene conocerlas antes de necesitarlas:
| Comando | Efecto |
|---|---|
git cherry-pick --continue |
Cierra el trasplante con la resolución preparada |
git cherry-pick --abort |
Cancela y devuelve la rama al estado previo |
git cherry-pick --skip |
Descarta este commit y sigue con el resto del rango |
--abort deja el repositorio exactamente como estaba, con el árbol limpio y el archivo con su contenido anterior.
Parte 4: trasplantar un rango
Para llevar varias correcciones a una rama de mantenimiento, se puede indicar un rango. Ojo con la sintaxis: A..B excluye A, así que para incluir el primer commit se escribe A^..B.
git switch produccion
git cherry-pick <hash-primer-fix>^..<hash-último-fix>[produccion b4895d2] fix: valida el email
1 file changed, 1 insertion(+)
create mode 100644 validacion.py
[produccion 52a7892] fix: corrige el mensaje de error
1 file changed, 1 insertion(+)
create mode 100644 errores.pygit log --oneline52a7892 fix: corrige el mensaje de error
b4895d2 fix: valida el email
dcdb3fb chore: inicia proyectoLa rama de producción recibe las dos correcciones y ninguna de las funcionalidades que había entre ellas. Es el caso de uso que justifica el comando.
Qué cambia Git
- Directorio de trabajo e índice: se aplican los cambios del commit trasplantado; si hay conflicto, quedan con marcadores hasta que se resuelva.
- Historial local: se añade un commit nuevo en la rama actual. El commit original no se toca ni desaparece de su rama.
- Remoto: no cambia hasta un
push. A diferencia del rebase, aquí no se reescribe nada ya existente, así que publicar no exige coordinación especial.
Y después, ¿qué pasa al integrar la rama?
Es la duda que siempre aparece: si más adelante hago merge de feat/rediseno, ¿se aplicará la corrección dos veces?
Primero, comprueba qué queda pendiente:
git cherry -v main feat/rediseno+ 273e6fc feat: añade descuento
- 2302bdf fix: corrige el IVA
+ 6df97a4 feat: nueva paletagit cherry compara los cambios, no los hashes. El signo - marca el commit cuyo contenido ya está en main; los + son los que faltan. Ahora integra:
git merge feat/rediseno -m "merge: integra el rediseño"
cat iva.pyMerge made by the 'ort' strategy.
descuento.py | 1 +
tema.py | 1 +
2 files changed, 2 insertions(+)
IVA = 0.21Solo se añaden los dos archivos que faltaban: el cambio no se duplica. Git detecta que ese contenido ya está aplicado. En el grafo verás la corrección como dos commits distintos, uno en cada línea, pero el archivo resultante es correcto.
Con todo, si el commit trasplantado se modifica después en su rama de origen, la reconciliación sí puede dar conflicto. Por eso -x importa: te dice de dónde salió cada cosa cuando toca investigar.
Errores frecuentes
- Usar
cherry-picken lugar demergepara integrar una rama entera. Duplica commits y ensucia el historial. Para integrar,merge. - Olvidar
-xal trasplantar entre ramas de larga vida. Meses después nadie sabe de dónde vino esa corrección. - Escribir
A..Besperando que incluyaA. El rango excluye el primer commit; hay que usarA^..B. - Trasplantar un commit que depende de otro anterior. El cambio se aplica, pero puede no funcionar porque le falta el commit del que dependía. Comprueba que el resultado funciona, no solo que Git no protestó.
- Resolver el conflicto y olvidar
git cherry-pick --continue. El repositorio se queda a medias;git statuste lo recuerda. - Confundir el marcador
>>>>>>> <hash> (mensaje)con una rama. En un cherry-pick el marcador identifica el commit trasplantado.
Ejercicio y criterios
Crea un repositorio con una rama main y una rama feat/informes que contenga cuatro commits: dos de funcionalidad, uno de corrección de un error y otro de documentación.
- Trasplanta a
mainúnicamente el commit de corrección, con-x. - Demuestra que el hash cambió y que el contenido del cambio es idéntico.
- Provoca un conflicto trasplantando un segundo commit y resuélvelo con
--continue. Repite el trasplante desde cero y esta vez cancélalo con--abort, verificando que el repositorio queda como antes. - Crea una rama
mantenimientoa partir de un commit antiguo y trasplanta a ella un rango de dos commits conA^..B. - Ejecuta
git cherry -v main feat/informese interpreta los signos. - Integra
feat/informesenmainconmergey comprueba si el cambio trasplantado se duplicó.
Se considera correcto si:
maincontiene la corrección y ninguna de las funcionalidades de la rama.- El mensaje del commit trasplantado incluye la línea
(cherry picked from commit …). - Se explica por qué el hash es distinto aunque el cambio sea el mismo.
- Tras el
--abort,git statusqueda limpio y el contenido coincide con el previo. - La rama
mantenimientorecibe exactamente los dos commits del rango. - Se interpreta correctamente el
-degit cherryy se explica por qué el merge final no duplicó el contenido. - No se ha modificado ningún remoto.
Laboratorio C — Encontrar una regresión con git bisect
Objetivo
Identificar el primer commit malo entre un commit que se sabe correcto y una versión actual que falla.
Idea clave
git bisect realiza una búsqueda binaria: comprueba un commit intermedio, se marca como good o bad y Git reduce el rango. No reescribe el historial; durante la búsqueda cambia temporalmente el HEAD a los commits que hay que probar.
Preparación reproducible
git init -b main laboratorio-bisect
cd laboratorio-bisect
git config user.name "Alumno"
git config user.email "alumno@example.test"
printf "resultado=4\n" > calculadora.txt
git add calculadora.txt
git commit -m "feat: calcula suma correcta"
printf "# Calculadora\n" > README.md
git add README.md
git commit -m "docs: añade título"
printf "resultado=5\n" > calculadora.txt
git add calculadora.txt
git commit -m "refactor: ajusta cálculo"
printf "Uso interno.\n" >> README.md
git add README.md
git commit -m "docs: añade nota de uso"
printf "Formato revisado.\n" >> README.md
git add README.md
git commit -m "docs: revisa formato"El criterio de prueba será sencillo: calculadora.txt debe contener exactamente resultado=4.
grep -qx "resultado=4" calculadora.txtEn la punta actual no habrá salida y el comando devolverá un código de error: la regresión está presente.
Búsqueda guiada
git bisect start
git bisect bad
git bisect good HEAD~4Git cambia a un commit intermedio. En cada parada, ejecutar:
grep -qx "resultado=4" calculadora.txtSi el comando no muestra nada y falla, el commit es malo:
git bisect badSi el comando termina correctamente, el commit es bueno:
git bisect good
Repetir hasta obtener una salida de este tipo:
<hash> is the first bad commit
commit <hash>
Author: Alumno <alumno@example.test>
Date: ...
refactor: ajusta cálculoComprobar el cambio:
git show --stat
git showFinalizar siempre la sesión:
git bisect resetSalida esperada:
Previous HEAD position was <hash> ...
Switched to branch 'main'Qué cambia Git
- Directorio de trabajo e índice: se actualizan en cada comprobación para reflejar el commit que se está probando.
- Historial local: no cambia; solo se mueve temporalmente
HEAD. - Remoto: no cambia.
- Al terminar:
git bisect resetdevuelve el repositorio a la rama o commit desde el que se inició el diagnóstico.
Automatización opcional
Cuando existe una prueba automatizada que devuelve 0 si pasa y un valor distinto de 0 si falla:
git bisect start HEAD HEAD~4
git bisect run grep -qx "resultado=4" calculadora.txt
git bisect resetEn un proyecto real, se sustituye grep ... por el comando de pruebas del proyecto, por ejemplo npm test, pytest o mvn test, siempre que su código de salida represente correctamente éxito o fallo.
Errores frecuentes
Marcar como
goodun commit que no se ha probado.Elegir un commit “bueno” que no es anterior al fallo.
Confundir un fallo de compilación ajeno a la regresión con un resultado
bad. Si no se puede evaluar un commit, usar:git bisect skipOlvidar
git bisect resety continuar trabajando conHEADseparado.
Ejercicio y criterios
Introduce deliberadamente una regresión en el cuarto de siete commits de una rama. Define una comprobación reproducible, localiza el primer commit malo y documenta:
- El hash y mensaje del primer commit malo.
- El último commit bueno utilizado.
- La comprobación aplicada.
- La salida de
git bisect reset.
Se considera correcto si el commit señalado es el que introdujo el fallo, no uno posterior que solo lo conserva.
Resumen final
El alumnado debe saber:
- decidir cuándo un
rebasees apropiado y cuándo no; - reorganizar una rama privada con
rebase -i; - continuar, abortar o recuperar un rebase;
- trasplantar un commit concreto con
cherry-picky explicar por qué el hash cambia; - elegir entre
cherry-pick,mergeyrebasesegún el problema; - dejar trazabilidad con
-xy leer la salida degit cherry; - delimitar una regresión entre un commit bueno y uno malo;
- usar
git bisectsin alterar el historial ni el remoto; - cerrar siempre una sesión de
bisect.
Los tres laboratorios comparten una idea de fondo: Git no mueve commits, los reaplica. rebase lo hace con una rama entera, cherry-pick con uno suelto, y bisect no reaplica nada porque solo diagnostica.
Estado editorial: listo.
Registro de revisión técnica
Revisado el 4 de agosto de 2026 con Git 2.47.1. Los tres laboratorios se han reproducido desde carpetas vacías, con identidad de práctica y configuración aislada.
Corrección aplicada al laboratorio A. La preparación original hacía que el commit de main (docs: añade guía de desarrollo) añadiera texto al final del mismo README.md que modificaba la rama, de modo que git rebase main siempre terminaba en CONFLICT (content): Merge conflict in README.md y no en el Successfully rebased que el documento anunciaba. El alumnado se topaba con un conflicto en la Parte 1, antes de que el documento explicara cómo resolverlo. Se ha cambiado ese commit para que cree CONTRIBUTING.md: el rebase se aplica limpio, la salida coincide con la documentada y la sección de conflictos conserva su papel. Verificado que git rebase main devuelve Successfully rebased and updated refs/heads/feature/formulario. con el árbol limpio, y que git rebase -i main con squash en la segunda línea deja los dos commits esperados.
Laboratorio B, nuevo. Se ha comprobado que:
git cherry-pickcrea un commit nuevo: eldiffdel original y el del trasplantado son idénticos y sus hashes distintos.-xañade al mensaje la línea(cherry picked from commit <hash completo>).- Ante un conflicto,
git status --shortmarcaUUy el marcador inferior identifica el commit trasplantado, no una rama;git statusofrece--continue,--skipy--abort, y--abortrestituye el estado previo con el árbol limpio. - El rango
A^..Btrasplanta los dos commits indicados sin arrastrar los intermedios de otra naturaleza. git cherry -v main feat/redisenomarca con-el commit ya aplicado y con+los pendientes.- Un
mergeposterior de la rama de origen no duplica el contenido trasplantado: solo incorpora los archivos que faltaban.
Laboratorio C verificado sin cambios: la búsqueda manual identifica refactor: ajusta cálculo como primer commit malo en dos pasos, git bisect reset devuelve a main, y la versión automatizada con git bisect run grep -qx … llega al mismo commit y termina con bisect found first bad commit.
Los hashes de las salidas son los de esta ejecución y serán distintos en cada repositorio.